UNPKG

@stackbit/utils

Version:
275 lines (264 loc) 9.14 kB
import http, { IncomingMessage } from 'http'; import type * as StackbitTypes from '@stackbit/types'; export type FlatDocument = { __metadata: Omit<StackbitTypes.Document, 'fields'>; [key: string]: any; }; export type GetCSIDocumentsOptions = { stackbitApiKey: string; srcType?: string; srcProjectId?: string; srcDocumentIds?: string[]; documentSpecs?: StackbitTypes.DocumentSpecifier[]; limit?: number; offset?: number; }; export async function getCSIDocuments(options: GetCSIDocumentsOptions & { flatDocuments?: never; flatLocale?: never }): Promise<{ total: number; offset: number; documents: StackbitTypes.Document[]; }>; export async function getCSIDocuments(options: GetCSIDocumentsOptions & { flatDocuments?: boolean; flatLocale?: string }): Promise<{ total: number; offset: number; documents: FlatDocument[]; }>; export async function getCSIDocuments({ stackbitApiKey, srcDocumentIds, srcType, srcProjectId, documentSpecs, limit, offset, flatDocuments, flatLocale }: GetCSIDocumentsOptions & { flatDocuments?: boolean; flatLocale?: string }) { const response = await new Promise<{ total: number; offset: number; documents: StackbitTypes.Document[]; }>((resolve, reject) => { const url = `${process.env.STACKBIT_HOST ?? 'http://localhost:8090'}/_stackbit/getCSIDocuments`; const req = http.request( url, { method: 'POST', headers: { Authorization: `Bearer ${stackbitApiKey}`, 'Content-Type': 'application/json' } }, (res: IncomingMessage) => { let output: string = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { output += chunk; }); res.on('end', () => { resolve(JSON.parse(output)); }); } ); req.on('error', (error) => { console.log(`error getting documents from stackbit: ${error.message}`); reject(error); }); if (srcType && srcProjectId && srcDocumentIds && srcDocumentIds.length > 0) { documentSpecs = srcDocumentIds.map((srcDocumentId) => ({ srcType, srcProjectId, srcDocumentId })); } if (documentSpecs) { // get the specified documents req.write(JSON.stringify({ documentSpecs, limit, offset })); } else { // get all documents, possibly filtered by srcType and/or srcProjectId req.write(JSON.stringify({ srcType, srcProjectId, limit, offset })); } req.end(); }); if (flatDocuments) { return { total: response.total, offset: response.offset, documents: response.documents.map((document) => flattenDocument({ document, locale: flatLocale })) }; } return response; } /** * Flattens {@link StackbitTypes.Document} into a simplified object. * All document properties except `fields` are moved into the `__metadata` * property. All document `fields` are placed at the root of the document and * recursively simplified by referencing their values directly. * * If the `locale` argument is not specified, only the non-localized fields will * be returned. If the `locale` is specified, only the localized document fields * for that locale, and also non-localized fields will be returned. * * @example * flattenDocument({ * document: { * type: 'document', * id: 'xyz', * modelName: 'Page', * ...other, * fields: { * title: { type: 'string', value: 'Welcome' }, * seo: { type: 'object', fields: { title: { type: 'string', value: 'SEO Welcome' } } }, * tags: { type: 'list', items: [{ type: 'string', value: 'tech' }] } * } * } * }) => { * __metadata: { * type: 'document', * id: 'xyz', * modelName: 'Page', * ...rest * }, * title: 'Welcome', * seo: { title: 'SEO Welcome' }, * tags: ['tech'] * } * * @param document * @param locale */ export function flattenDocument({ document, locale }: { document: StackbitTypes.Document; locale?: string }): FlatDocument { const { fields, ...rest } = document; const flattenedFields = flattenDocumentFields({ documentFields: fields, locale }); return { __metadata: rest, ...flattenedFields }; } function flattenDocumentFields({ documentFields, locale }: { documentFields?: Record<string, StackbitTypes.DocumentField>; locale?: string; }): Record<string, any> { return Object.entries(documentFields ?? {}).reduce( (flattenedFields: Record<string, any>, [fieldName, documentField]: [string, StackbitTypes.DocumentField]) => { const value = getDocumentFieldValue({ documentField, locale }); if (typeof value !== 'undefined') { flattenedFields[fieldName] = value; } return flattenedFields; }, {} ); } /** * Returns the value of a document field. * * If the document field is a primitive (string, url, slug, text, markdown, enum, date, number, boolean, etc.) * then the value of the `value` property is returned. * * If the document is a field of type "object", "model" or "list", the value of the * nested object is simplified and returned. * * If the document is a field of type "reference", the ID of the referenced * document is returned. * * If the document field is localized, then the value for the provided "locale" * is returned. If "locale" was not provided, or the document field does not * have any values for the provided "locale", undefined is returned. */ export function getDocumentFieldValue({ documentField, locale }: { documentField: StackbitTypes.DocumentField; locale?: string }): any { const nonLocalizedDocField = getLocalizedFieldForLocale(documentField, locale); if (typeof nonLocalizedDocField === 'undefined') { return nonLocalizedDocField; } switch (nonLocalizedDocField.type) { case 'string': case 'url': case 'slug': case 'text': case 'markdown': case 'html': case 'enum': case 'date': case 'datetime': case 'color': case 'file': case 'number': case 'boolean': case 'json': case 'style': case 'richText': { return nonLocalizedDocField.value; } case 'image': { const imageFields = flattenDocumentFields({ documentFields: nonLocalizedDocField.fields, locale }); return imageFields?.url ?? nonLocalizedDocField.sourceData; } case 'object': { return flattenDocumentFields({ documentFields: nonLocalizedDocField.fields, locale }); } case 'model': { return { __metadata: { modelName: nonLocalizedDocField.modelName }, ...flattenDocumentFields({ documentFields: nonLocalizedDocField.fields, locale }) }; } case 'reference': { return nonLocalizedDocField.refId; } case 'cross-reference': { return { refId: nonLocalizedDocField.refId, refSrcType: nonLocalizedDocField.refSrcType, refProjectId: nonLocalizedDocField.refProjectId }; } case 'list': { return nonLocalizedDocField.items.map((item) => { return getDocumentFieldValue({ documentField: item, locale }); }); } default: { const _exhaustiveCheck: never = nonLocalizedDocField; return _exhaustiveCheck; } } } // This method was copied from @stackbit/types => getLocalizedFieldForLocale // instead of importing it directly, to allow bundling this file (document-utils.ts) // by Webpack without bundling the whole @stackbit/types when it is imported // inside frameworks like Next.js. function getLocalizedFieldForLocale<Type extends StackbitTypes.FieldType>( field?: StackbitTypes.DocumentFieldForType<Type>, locale?: string ): StackbitTypes.DocumentFieldNonLocalizedForType<Type> | undefined { if (field && field.localized) { if (!locale) { return undefined; } const { localized, locales, ...base } = field; const localizedField = locales?.[locale]; if (!localizedField) { return undefined; } return { ...base, ...localizedField } as unknown as StackbitTypes.DocumentFieldNonLocalizedForType<Type>; } return field; }