@stackbit/utils
Version:
Stackbit utilities
275 lines (264 loc) • 9.14 kB
text/typescript
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;
}