studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
1,247 lines (1,093 loc) • 43 kB
text/typescript
import { asc, eq, or } from 'astro:db';
import config from 'studiocms:config';
import {
CMSNotificationSettingsId,
GhostUserDefaults,
versionCacheLifetime,
} from '../../../consts.js';
import { Effect, genLogger } from '../../../effect.js';
import {
FolderListMapID,
FolderTreeMapID,
PageFolderTreeMapID,
SiteConfigMapID,
StudioCMSPkgId,
VersionMapID,
} from '../consts.js';
import {
AstroDB,
GetVersionFromNPM,
SDKCore_Collectors,
SDKCore_FolderTree,
SDKCore_Users,
} from '../effect/index.js';
import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js';
import {
tsDiffTracking,
tsEmailVerificationTokens,
tsNotificationSettings,
tsOAuthAccounts,
tsPageContent,
tsPageData,
tsPageDataCategories,
tsPageDataTags,
tsPageFolderStructure,
tsPermissions,
tsSessionTable,
tsUsers,
} from '../tables.js';
import type {
CombinedPageData,
CombinedUserData,
FolderNode,
MetaOnlyPageData,
MetaOnlyPageDataCacheObject,
PageDataCacheObject,
PaginateInput,
} from '../types/index.js';
import {
_ClearUnknownError,
_clearLibSQLError,
CacheContext,
convertCombinedPageDataToMetaOnly,
filterPagesByDraftAndIndex,
folderListReturn,
folderTreeReturn,
isCacheEnabled,
isCacheExpired,
pageDataReturn,
siteConfigReturn,
versionReturn,
} from '../utils.js';
import { SDKCore_CONFIG } from './config.js';
/**
* The `SDKCore_GET` service provides a comprehensive set of data retrieval methods for the StudioCMS SDK.
* It encapsulates database queries, caching logic, and transformation utilities for core CMS entities such as pages, users, folders, permissions, and site configuration.
*
* @remarks
* - All methods are wrapped in `Effect` for composable error handling and dependency injection.
* - Caching is used for performance optimization, with cache invalidation and fallback to database queries.
* - Pagination, filtering, and meta-only retrieval are supported for page queries.
*
* @example
* ```typescript
* const pages = yield* GET.pages();
* const user = yield* GET.users.byId("user-id");
* const folderTree = yield* GET.folderTree();
* ```
*
* @service studiocms/sdk/SDKCore/modules/get
* @dependencies
* - AstroDB.Default
* - SDKCore_FolderTree.Default
* - GetVersionFromNPM.Default
* - SDKCore_Users.Default
* - SDKCore_Collectors.Default
*
* @effect
* Provides the following main query groups:
* - `databaseTable`: Direct access to raw database tables.
* - `permissionsLists`: Retrieve users by permission rank.
* - `users`: Retrieve user data by various identifiers.
* - `folder`, `folderTree`, `folderList`, `pageFolderTree`: Folder and tree structure queries.
* - `siteConfig`, `latestVersion`: Site configuration and version info.
* - `page`: Retrieve page data by ID or slug.
* - `folderPages`, `packagePages`, `pages`: Retrieve pages by folder, package, or all pages.
*
* @throws {SDKCoreError} If a database or unknown error occurs during retrieval.
*/
export class SDKCore_GET extends Effect.Service<SDKCore_GET>()(
'studiocms/sdk/SDKCore/modules/get',
{
dependencies: [
AstroDB.Default,
SDKCore_FolderTree.Default,
GetVersionFromNPM.Default,
SDKCore_Users.Default,
SDKCore_Collectors.Default,
SDKCore_CONFIG.Default,
],
effect: genLogger('studiocms/sdk/SDKCore/modules/get/effect')(function* () {
const [
dbService,
{ FolderList, pages, folderTree, version, siteConfig, pageFolderTree },
{ buildFolderTree, getAvailableFolders, addPageToFolderTree },
getVersionFromNPM,
{ combineRanks, verifyRank },
{ collectUserData, collectPageData },
{ siteConfig: sdkSiteConfig },
] = yield* Effect.all([
AstroDB,
CacheContext,
SDKCore_FolderTree,
GetVersionFromNPM,
SDKCore_Users,
SDKCore_Collectors,
SDKCore_CONFIG,
]);
/**
* Validates the pagination input by ensuring that `limit` and `offset` are non-negative values.
* If `limit` is zero, it sets a default value of 10.
* Returns the validated (and possibly modified) pagination input.
* If validation fails, yields an `SDKCoreError` with a descriptive message.
*
* @template P - Type extending `PaginateInput`.
* @param paginate - The pagination input to validate.
* @returns The validated pagination input or fails with an error if validation fails.
*/
const validatePagination = Effect.fn(function* <P extends PaginateInput>(paginate: P) {
if (paginate.limit < 0 || paginate.offset < 0) {
return yield* Effect.fail(
new SDKCoreError({
type: 'UNKNOWN',
cause: new StudioCMS_SDK_Error(
'Pagination limit and offset must be non-negative values'
),
})
);
}
if (paginate.limit === 0) {
paginate.limit = 10; // Default value
}
return paginate;
});
/**
* Retrieves a folder from the database by its ID or name.
*
* This function queries the `tsPageFolderStructure` table for a folder whose `id` or `name`
* matches the provided `idOrName` parameter. If no matching folder is found, it fails with a
* `LibSQLDatabaseError`.
*
* @param idOrName - The ID or name of the folder to retrieve.
* @returns The folder data if found.
* @throws {SDKCoreError} If the folder is not found in the database.
*/
const getFolderByNameOrId = Effect.fn(function* (idOrName: string) {
const data = yield* dbService.execute((db) =>
db
.select()
.from(tsPageFolderStructure)
.where(
or(eq(tsPageFolderStructure.id, idOrName), eq(tsPageFolderStructure.name, idOrName))
)
.get()
);
if (!data) {
return yield* Effect.fail(
new SDKCoreError({
type: 'LibSQLDatabaseError',
cause: new StudioCMS_SDK_Error(`Folder not found in Database: ${idOrName}`),
})
);
}
return data;
});
function _getPackagesPages(
packageName: string,
tree?: FolderNode[]
): Effect.Effect<CombinedPageData[], SDKCoreError, never>;
function _getPackagesPages(
packageName: string,
tree?: FolderNode[],
metaOnly?: boolean
): Effect.Effect<MetaOnlyPageData[], SDKCoreError, never>;
/**
* Retrieves all pages associated with a given package name, optionally using a provided folder tree and returning only metadata if specified.
*
* @param packageName - The name of the package to fetch pages for.
* @param tree - Optional. A pre-built folder tree to use for organizing pages. If not provided, the folder tree will be built automatically.
* @param metaOnly - Optional. If true, returns only metadata for each page; otherwise, returns combined page data. Defaults to false.
* @returns An Effect that resolves to an array of page data, either as `MetaOnlyPageData[]` or `CombinedPageData[]` depending on `metaOnly`.
*/
function _getPackagesPages(packageName: string, tree?: FolderNode[], metaOnly = false) {
return Effect.gen(function* () {
const pagesRaw = yield* dbService.execute((db) =>
db.select().from(tsPageData).where(eq(tsPageData.package, packageName))
);
const folders = tree || (yield* buildFolderTree);
const pages = [];
for (const page of pagesRaw) {
const data = yield* collectPageData(page, folders, metaOnly);
pages.push(data);
}
if (metaOnly) {
return pages as MetaOnlyPageData[];
}
return pages as CombinedPageData[];
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.packagePages', cause),
})
);
}
function _getPageById(
id: string
): Effect.Effect<PageDataCacheObject | undefined, SDKCoreError, never>;
function _getPageById(
id: string,
metaOnly?: boolean
): Effect.Effect<MetaOnlyPageDataCacheObject | undefined, SDKCoreError, never>;
/**
* Retrieves a page by its ID, with an option to return only metadata.
*
* @param id - The ID of the page to retrieve.
* @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`.
* @returns An `Effect` that yields the page data or metadata, or `undefined` if no page is found.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
function _getPageById(id: string, metaOnly = false) {
const getPage = (id: string, tree?: FolderNode[]) =>
Effect.gen(function* () {
const page = yield* dbService.execute((db) =>
db.select().from(tsPageData).where(eq(tsPageData.id, id)).get()
);
if (!page) return undefined;
const folders = tree || (yield* buildFolderTree);
return yield* collectPageData(page, folders);
});
return Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const page = yield* getPage(id);
if (!page) {
return undefined;
}
const pageData = pageDataReturn(page);
return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData;
}
const { data: tree } = yield* GET.folderTree();
const cachedPage = pages.get(id);
if (!cachedPage || isCacheExpired(cachedPage)) {
const page = yield* getPage(id, tree);
if (!page) {
return undefined;
}
const returnPage = pageDataReturn(page);
pages.set(id, returnPage);
return metaOnly ? convertCombinedPageDataToMetaOnly(returnPage) : returnPage;
}
// Return the cached page
return metaOnly ? convertCombinedPageDataToMetaOnly(cachedPage) : cachedPage;
}).pipe(
Effect.catchTags({
UnknownException: (cause: unknown) => _clearLibSQLError('GET.page.byId', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.page.byId', cause),
})
);
}
function _getPageBySlug(
slug: string
): Effect.Effect<PageDataCacheObject | undefined, SDKCoreError, never>;
function _getPageBySlug(
slug: string,
metaOnly?: boolean
): Effect.Effect<MetaOnlyPageDataCacheObject | undefined, SDKCoreError, never>;
/**
* Retrieves a page by its slug, with an option to return only metadata.
*
* @param slug - The slug of the page to retrieve.
* @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`.
* @returns An `Effect` that yields the page data or metadata, or `undefined` if no page is found.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
function _getPageBySlug(slug: string, metaOnly = false) {
const getPage = (iSlug: string, tree?: FolderNode[]) =>
Effect.gen(function* () {
const page = yield* dbService.execute((db) =>
db.select().from(tsPageData).where(eq(tsPageData.slug, iSlug)).get()
);
if (!page) return undefined;
const folders = tree || (yield* buildFolderTree);
return yield* collectPageData(page, folders);
});
return Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const page = yield* getPage(slug);
if (!page) {
return undefined;
}
const pageData = pageDataReturn(page);
return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData;
}
const { data: tree } = yield* GET.folderTree();
// Retrieve the cached page
const cachedPage = Array.from(pages.values()).find((page) => page.data.slug === slug);
if (!cachedPage || isCacheExpired(cachedPage)) {
const page = yield* getPage(slug, tree);
if (!page) {
return undefined;
}
const pageData = pageDataReturn(page);
pages.set(page.id, pageData);
return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData;
}
// Return the cached page
return metaOnly ? convertCombinedPageDataToMetaOnly(cachedPage) : cachedPage;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.page.bySlug', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.page.bySlug', cause),
})
);
}
function _getAllPages(
includeDrafts?: boolean,
hideDefaultIndex?: boolean,
metaOnly?: false,
paginate?: PaginateInput
): Effect.Effect<PageDataCacheObject[], SDKCoreError, never>;
function _getAllPages(
includeDrafts?: boolean,
hideDefaultIndex?: boolean,
metaOnly?: true,
paginate?: PaginateInput
): Effect.Effect<MetaOnlyPageDataCacheObject[], SDKCoreError, never>;
/**
* Retrieves all pages from the database or cache, with options for including drafts,
* hiding the default index page, returning only metadata, and paginating results.
*
* - If caching is enabled, attempts to return cached pages and updates expired cache entries.
* - If caching is disabled, fetches pages directly from the database.
* - Supports filtering by draft status and default index visibility.
* - Can return either full page data or metadata only.
* - Supports pagination via the `paginate` parameter.
*
* @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`.
* @param hideDefaultIndex - Whether to exclude the default index page from the results. Defaults to `false`.
* @param metaOnly - Whether to return only metadata for each page. Defaults to `false`.
* @param paginate - Optional pagination input specifying `limit` and `offset`.
* @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`.
*/
function _getAllPages(
includeDrafts = false,
hideDefaultIndex = false,
metaOnly = false,
paginate?: PaginateInput
) {
const getPages = (
includeDrafts = false,
hideDefaultIndex = false,
tree?: FolderNode[],
paginate?: PaginateInput
) =>
Effect.gen(function* () {
let pagesRaw: (typeof tsPageData.$inferSelect)[] = [];
if (paginate) {
const paginateData = yield* validatePagination(paginate);
pagesRaw = yield* dbService.execute((db) =>
db
.select()
.from(tsPageData)
.orderBy(asc(tsPageData.title))
.limit(paginateData.limit)
.offset(paginateData.offset)
);
} else {
pagesRaw = yield* dbService.execute((db) =>
db.select().from(tsPageData).orderBy(asc(tsPageData.title))
);
}
const pagesFiltered = filterPagesByDraftAndIndex(
pagesRaw,
includeDrafts,
hideDefaultIndex
);
const folders = tree || (yield* buildFolderTree);
const pages = [];
for (const page of pagesFiltered) {
const data = yield* collectPageData(page, folders);
pages.push(data);
}
return pages;
});
return Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const dbPages = yield* getPages(includeDrafts, hideDefaultIndex, undefined, paginate);
const data = dbPages.map((page) => pageDataReturn(page));
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}
const { data: tree } = yield* GET.folderTree();
if (pages.size === 0) {
const newData = yield* getPages(includeDrafts, hideDefaultIndex, tree);
// Loop through the updated data and store it in the cache
for (const data of newData) {
pages.set(data.id, pageDataReturn(data));
}
const data = newData.map((data) => pageDataReturn(data));
if (paginate) {
const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title));
const paginatedData = sortedData.slice(
paginate.offset,
paginate.offset + paginate.limit
);
return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData;
}
// Transform and return the data
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}
const cacheMap = Array.from(pages.values());
for (const item of cacheMap) {
if (isCacheExpired(item)) {
const updated = yield* GET.page.byId(item.data.id);
if (!updated) {
pages.delete(item.data.id);
continue;
}
pages.set(updated.data.id, updated);
}
}
const data = Array.from(pages.values());
if (paginate) {
const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title));
const paginatedData = sortedData.slice(
paginate.offset,
paginate.offset + paginate.limit
);
return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData;
}
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.pages', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.pages', cause),
})
);
}
function _folderPages(
idOrName: string,
includeDrafts?: boolean,
hideDefaultIndex?: boolean,
metaOnly?: false,
paginate?: PaginateInput
): Effect.Effect<PageDataCacheObject[], SDKCoreError, never>;
function _folderPages(
idOrName: string,
includeDrafts?: boolean,
hideDefaultIndex?: boolean,
metaOnly?: true,
paginate?: PaginateInput
): Effect.Effect<MetaOnlyPageDataCacheObject[], SDKCoreError, never>;
/**
* Retrieves pages within a specified folder by its ID or name, supporting various filtering and pagination options.
*
* This function handles both cached and non-cached scenarios, fetching pages from the database or cache as appropriate.
* It supports filtering drafts, hiding default index pages, returning only metadata, and paginating results.
*
* @param idOrName - The ID or name of the folder to retrieve pages from.
* @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`.
* @param hideDefaultIndex - Whether to exclude default index pages from the results. Defaults to `false`.
* @param metaOnly - If `true`, returns only metadata for each page. Defaults to `false`.
* @param paginate - Optional pagination input specifying `limit` and `offset`.
* @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
function _folderPages(
idOrName: string,
includeDrafts = false,
hideDefaultIndex = false,
metaOnly = false,
paginate?: PaginateInput
) {
const getAllPages = (
includeDrafts = false,
hideDefaultIndex = false,
tree?: FolderNode[],
paginate?: PaginateInput
) =>
Effect.gen(function* () {
let pagesRaw: (typeof tsPageData.$inferSelect)[] = [];
if (paginate) {
const paginateData = yield* validatePagination(paginate);
pagesRaw = yield* dbService.execute((db) =>
db
.select()
.from(tsPageData)
.orderBy(asc(tsPageData.title))
.limit(paginateData.limit)
.offset(paginateData.offset)
);
} else {
pagesRaw = yield* dbService.execute((db) =>
db.select().from(tsPageData).orderBy(asc(tsPageData.title))
);
}
const pagesFiltered = filterPagesByDraftAndIndex(
pagesRaw,
includeDrafts,
hideDefaultIndex
);
const folders = tree || (yield* buildFolderTree);
const pages = [];
for (const page of pagesFiltered) {
const data = yield* collectPageData(page, folders);
pages.push(data);
}
return pages;
});
const getPages = (
id: string,
includeDrafts = false,
hideDefaultIndex = false,
tree?: FolderNode[]
) =>
Effect.gen(function* () {
const pagesRaw = yield* dbService.execute((db) =>
db
.select()
.from(tsPageData)
.where(eq(tsPageData.parentFolder, id))
.orderBy(asc(tsPageData.title))
);
const pagesFiltered = filterPagesByDraftAndIndex(
pagesRaw,
includeDrafts,
hideDefaultIndex
);
const folders = tree || (yield* buildFolderTree);
const pages = [];
for (const page of pagesFiltered) {
pages.push(yield* collectPageData(page, folders));
}
return pages;
});
return Effect.gen(function* () {
const status = yield* isCacheEnabled;
const folderData = yield* getFolderByNameOrId(idOrName);
if (!status) {
const dbPages = yield* getPages(folderData.id, includeDrafts, hideDefaultIndex);
const data = dbPages.map((page) => pageDataReturn(page));
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}
const { data: tree } = yield* GET.folderTree();
if (pages.size === 0) {
const updatedData = yield* getAllPages(includeDrafts, hideDefaultIndex, tree);
for (const data of updatedData) {
pages.set(data.id, pageDataReturn(data));
}
const data = updatedData
.filter(({ parentFolder }) => parentFolder === folderData.id)
.map((data) => pageDataReturn(data));
if (paginate) {
const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title));
const paginatedData = sortedData.slice(
paginate.offset,
paginate.offset + paginate.limit
);
return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData;
}
// Transform and return the data
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}
const cacheMap = Array.from(pages.values());
for (const item of cacheMap) {
if (isCacheExpired(item)) {
const updated = yield* GET.page.byId(item.data.id);
if (!updated) {
pages.delete(item.data.id);
continue;
}
pages.set(updated.data.id, updated);
}
}
const data = Array.from(pages.values()).filter(
({ data: { parentFolder } }) => parentFolder === folderData.id
);
if (paginate) {
const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title));
const paginatedData = sortedData.slice(
paginate.offset,
paginate.offset + paginate.limit
);
return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData;
}
return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.pages', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.pages', cause),
})
);
}
/**
* Retrieves the list of permissions for users with the specified rank.
*
* This function queries the database for all permitted users and existing users,
* then verifies and returns the permissions associated with the given rank.
* Handles database errors by invoking a custom error handler.
*
* @param rank - The rank of users to retrieve permissions for. Must be one of 'owner', 'admin', 'editor', or 'visitor'.
* @returns An Effect that resolves to the verified permissions for the specified rank, or handles database errors.
*/
const getPermissionsByRank = (rank: 'owner' | 'admin' | 'editor' | 'visitor') =>
Effect.gen(function* () {
const currentPermittedUsers = yield* dbService.execute((db) =>
db.select().from(tsPermissions)
);
const existingUsers = yield* dbService.execute((db) => db.select().from(tsUsers));
return yield* verifyRank(existingUsers, currentPermittedUsers, rank);
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError(`GET.permissionsLists.${rank}s`, cause),
})
);
const GET = {
databaseTable: {
users: () => dbService.execute((db) => db.select().from(tsUsers)),
oAuthAccounts: () => dbService.execute((db) => db.select().from(tsOAuthAccounts)),
sessionTable: () => dbService.execute((db) => db.select().from(tsSessionTable)),
permissions: () => dbService.execute((db) => db.select().from(tsPermissions)),
pageData: () => dbService.execute((db) => db.select().from(tsPageData)),
pageDataTags: () => dbService.execute((db) => db.select().from(tsPageDataTags)),
pageDataCategories: () =>
dbService.execute((db) => db.select().from(tsPageDataCategories)),
pageContent: () => dbService.execute((db) => db.select().from(tsPageContent)),
diffTracking: () => dbService.execute((db) => db.select().from(tsDiffTracking)),
pageFolderStructure: () =>
dbService.execute((db) => db.select().from(tsPageFolderStructure)),
notificationSettings: () =>
dbService.execute((db) =>
db
.select()
.from(tsNotificationSettings)
.where(eq(tsNotificationSettings.id, CMSNotificationSettingsId))
.get()
),
emailVerificationTokens: () =>
dbService.execute((db) => db.select().from(tsEmailVerificationTokens)),
},
permissionsLists: {
/**
* Retrieves all permissions for users in the database.
*
* @returns A promise that resolves to an array of combined rank data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the permissions.
*/
all: () =>
Effect.gen(function* () {
const currentPermittedUsers = yield* dbService.execute((db) =>
db.select().from(tsPermissions)
);
const existingUsers = yield* dbService.execute((db) => db.select().from(tsUsers));
const owners = yield* verifyRank(existingUsers, currentPermittedUsers, 'owner');
const admins = yield* verifyRank(existingUsers, currentPermittedUsers, 'admin');
const editors = yield* verifyRank(existingUsers, currentPermittedUsers, 'editor');
const visitors = yield* verifyRank(existingUsers, currentPermittedUsers, 'visitor');
return [
...(yield* combineRanks('owner', owners)),
...(yield* combineRanks('admin', admins)),
...(yield* combineRanks('editor', editors)),
...(yield* combineRanks('visitor', visitors)),
];
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.permissionsLists.all', cause),
})
),
/**
* Retrieves all owners in the database.
*
* @returns A promise that resolves to an array of combined rank data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the owners.
*/
owners: () => getPermissionsByRank('owner'),
/**
* Retrieves all admins in the database.
*
* @returns A promise that resolves to an array of combined rank data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the admins.
*/
admins: () => getPermissionsByRank('admin'),
/**
* Retrieves all editors in the database.
*
* @returns A promise that resolves to an array of combined rank data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the editors.
*/
editors: () => getPermissionsByRank('editor'),
/**
* Retrieves all visitors in the database.
*
* @returns A promise that resolves to an array of combined rank data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the visitors.
*/
visitors: () => getPermissionsByRank('visitor'),
},
users: {
/**
* Retrieves all users from the database.
*
* @returns A promise that resolves to an array of combined user data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the users.
*/
all: () =>
Effect.gen(function* () {
const combinedUserData: CombinedUserData[] = [];
const users = yield* dbService.execute((db) => db.select().from(tsUsers));
for (const user of users) {
if (user.id === GhostUserDefaults.id) {
continue;
}
const UserData = yield* collectUserData(user);
combinedUserData.push(UserData);
}
return combinedUserData;
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.users.all', cause),
})
),
/**
* Retrieves a user by ID.
*
* @param id - The ID of the user to retrieve.
* @returns A promise that resolves to the user data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the user.
*/
byId: (id: string) =>
Effect.gen(function* () {
const user = yield* dbService.execute((db) =>
db.select().from(tsUsers).where(eq(tsUsers.id, id)).get()
);
if (!user) return undefined;
return yield* collectUserData(user);
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause),
})
),
/**
* Retrieves a user by username.
*
* @param username - The username of the user to retrieve.
* @returns A promise that resolves to the user data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the user.
*/
byUsername: (username: string) =>
Effect.gen(function* () {
const user = yield* dbService.execute((db) =>
db.select().from(tsUsers).where(eq(tsUsers.username, username)).get()
);
if (!user) return undefined;
return yield* collectUserData(user);
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause),
})
),
/**
* Retrieves a user by email.
*
* @param email - The email of the user to retrieve.
* @returns A promise that resolves to the user data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the user.
*/
byEmail: (email: string) =>
Effect.gen(function* () {
const user = yield* dbService.execute((db) =>
db.select().from(tsUsers).where(eq(tsUsers.email, email)).get()
);
if (!user) return undefined;
return yield* collectUserData(user);
}).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause),
})
),
},
/**
* Retrieves a folder by ID.
*
* @param id - The ID of the folder to retrieve.
* @returns A promise that resolves to the folder data.
* @throws {StudioCMS_SDK_Error} If an error occurs while getting the folder.
*/
folder: dbService.makeQuery((ex, id: string) =>
ex((db) =>
db.select().from(tsPageFolderStructure).where(eq(tsPageFolderStructure.id, id)).get()
).pipe(
Effect.catchTags({
LibSQLClientError: (cause) => _clearLibSQLError('GET.folder', cause),
})
)
),
/**
* Retrieves the site configuration, with caching support.
* If caching is enabled, it checks the cache for an existing site config.
* If the cache is expired or not available, it fetches the site config from the database
* and updates the cache.
* @returns An `Effect` that yields the site config.
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
siteConfig: () =>
Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (config.dbStartPage) {
return undefined;
}
if (!status) {
const newConfig = yield* sdkSiteConfig.get();
if (!newConfig) {
return yield* Effect.fail(
new SDKCoreError({
type: 'UNKNOWN',
cause: new StudioCMS_SDK_Error('Site config not found in database'),
})
);
}
return siteConfigReturn(newConfig.data);
}
const currentSiteConfig = siteConfig.get(SiteConfigMapID);
if (!currentSiteConfig || isCacheExpired(currentSiteConfig)) {
const newConfig = yield* sdkSiteConfig.get();
if (!newConfig) {
return yield* Effect.fail(
new SDKCoreError({
type: 'UNKNOWN',
cause: new StudioCMS_SDK_Error('Site config not found in database'),
})
);
}
const returnConfig = siteConfigReturn(newConfig.data);
siteConfig.set(SiteConfigMapID, returnConfig);
return returnConfig;
}
return currentSiteConfig;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.siteConfig', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.siteConfig', cause),
})
),
/**
* Retrieves the folder tree structure, with caching support.
* If caching is enabled, it checks the cache for an existing folder tree.
* If the cache is expired or not available, it fetches the folder tree from the database
* and updates the cache.
* @returns An `Effect` that yields the folder tree.
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
folderTree: () =>
Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
return folderTreeReturn(yield* buildFolderTree);
}
const tree = folderTree.get(FolderTreeMapID);
if (!tree || isCacheExpired(tree)) {
const newFolderTree = yield* buildFolderTree;
const returnable = folderTreeReturn(newFolderTree);
folderTree.set(FolderTreeMapID, returnable);
return returnable;
}
return tree;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.folderTree', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.folderTree', cause),
})
),
/**
* Retrieves the folder tree structure, with caching support.
* If caching is enabled, it checks the cache for an existing folder tree.
* If the cache is expired or not available, it fetches the folder tree from the database
* and updates the cache.
* @returns An `Effect` that yields the folder tree.
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
pageFolderTree: (hideDefaultIndex = false) =>
Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const folderTree = yield* buildFolderTree;
const pages = yield* GET.pages(true, hideDefaultIndex);
for (const { data: page } of pages) {
if (page.parentFolder) {
yield* addPageToFolderTree(folderTree, page.parentFolder, {
id: page.id,
name: page.title,
page: true,
pageData: page,
children: [],
});
} else {
folderTree.push({
id: page.id,
name: page.title,
page: true,
pageData: page,
children: [],
});
}
}
return folderTreeReturn(folderTree);
}
const tree = pageFolderTree.get(PageFolderTreeMapID);
if (!tree || isCacheExpired(tree)) {
const newFolderTree = yield* buildFolderTree;
const pages = yield* GET.pages(true, hideDefaultIndex);
for (const { data: page } of pages) {
if (page.parentFolder) {
yield* addPageToFolderTree(newFolderTree, page.parentFolder, {
id: page.id,
name: page.title,
page: true,
pageData: page,
children: [],
});
} else {
newFolderTree.push({
id: page.id,
name: page.title,
page: true,
pageData: page,
children: [],
});
}
}
pageFolderTree.set(PageFolderTreeMapID, folderTreeReturn(newFolderTree));
return folderTreeReturn(newFolderTree);
}
return tree;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.pageFolderTree', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.pageFolderTree', cause),
})
),
/**
* Retrieves a list of available folders, with caching support.
* If caching is enabled, it checks the cache for an existing folder list.
* If the cache is expired or not available, it fetches the folder list from the database
* and updates the cache.
* @returns An `Effect` that yields the folder list.
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
folderList: () =>
Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const folderList = yield* getAvailableFolders;
return folderListReturn(folderList);
}
const list = FolderList.get(FolderListMapID);
if (!list || isCacheExpired(list)) {
const folderList = yield* getAvailableFolders;
FolderList.set(FolderListMapID, folderListReturn(folderList));
return folderListReturn(folderList);
}
return list;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _clearLibSQLError('GET.folderList', cause),
LibSQLClientError: (cause) => _clearLibSQLError('GET.folderList', cause),
})
),
/**
* Retrieves the latest version of StudioCMS from NPM, with caching support.
* @returns An `Effect` that yields the latest version of StudioCMS.
* @throws UnknownException - If an unknown error occurs during retrieval.
*/
latestVersion: () =>
Effect.gen(function* () {
const status = yield* isCacheEnabled;
if (!status) {
const version = yield* getVersionFromNPM.get(StudioCMSPkgId);
return versionReturn(version);
}
const latestVersion = version.get(VersionMapID);
if (!latestVersion || isCacheExpired(latestVersion, versionCacheLifetime)) {
const newVersion = yield* getVersionFromNPM.get(StudioCMSPkgId);
const latestVersion = versionReturn(newVersion);
version.set(VersionMapID, latestVersion);
return latestVersion;
}
return latestVersion;
}).pipe(
Effect.catchTags({
UnknownException: (cause) => _ClearUnknownError('GET.latestVersion', cause),
})
),
page: {
/**
* Retrieves a page by its ID, with an option to return only metadata.
*
* @param id - The ID of the page to retrieve.
* @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`.
* @returns An `Effect` that yields the page data or metadata, depending on `metaOnly`.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
byId: _getPageById,
/**
* Retrieves a page by its slug, with an option to return only metadata.
*
* @param slug - The slug of the page to retrieve.
* @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`.
* @returns An `Effect` that yields the page data or metadata, depending on `metaOnly`.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
bySlug: _getPageBySlug,
},
/**
* Retrieves pages within a specified folder by its ID or name, supporting various filtering and pagination options.
*
* This function handles both cached and non-cached scenarios, fetching pages from the database or cache as appropriate.
* It supports filtering drafts, hiding default index pages, returning only metadata, and paginating results.
*
* @param idOrName - The ID or name of the folder to retrieve pages from.
* @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`.
* @param hideDefaultIndex - Whether to exclude default index pages from the results. Defaults to `false`.
* @param metaOnly - If `true`, returns only metadata for each page. Defaults to `false`.
* @param paginate - Optional pagination input specifying `limit` and `offset`.
* @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`.
*
* @throws UnknownException - If an unknown error occurs during retrieval.
* @throws LibSQLDatabaseError - If a database error occurs during retrieval.
*/
folderPages: _folderPages,
/**
* Retrieves all pages associated with a given package name, optionally using a provided folder tree and returning only metadata if specified.
*
* @param packageName - The name of the package to fetch pages for.
* @param tree - Optional. A pre-built folder tree to use for organizing pages. If not provided, the folder tree will be built automatically.
* @param metaOnly - Optional. If true, returns only metadata for each page; otherwise, returns combined page data. Defaults to false.
* @returns An Effect that resolves to an array of page data, either as `MetaOnlyPageData[]` or `CombinedPageData[]` depending on `metaOnly`.
*/
packagePages: _getPackagesPages,
/**
* Retrieves all pages from the database or cache, with options for including drafts,
* hiding the default index page, returning only metadata, and paginating results.
*
* - If caching is enabled, attempts to return cached pages and updates expired cache entries.
* - If caching is disabled, fetches pages directly from the database.
* - Supports filtering by draft status and default index visibility.
* - Can return either full page data or metadata only.
* - Supports pagination via the `paginate` parameter.
*
* @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`.
* @param hideDefaultIndex - Whether to exclude the default index page from the results. Defaults to `false`.
* @param metaOnly - Whether to return only metadata for each page. Defaults to `false`.
* @param paginate - Optional pagination input specifying `limit` and `offset`.
* @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`.
*/
pages: _getAllPages,
};
return GET;
}),
}
) {}