zod-firebase-admin
Version:
zod firebase-admin schema
360 lines (337 loc) • 16.7 kB
JavaScript
let firebase_admin_firestore = require("firebase-admin/firestore");
//#region src/collection/factory/multi-document-collection-factory.ts
const multiDocumentCollectionFactory = (firestoreFactory, schema) => ({
...firestoreFactory,
findById: async (id, options) => {
return (await firestoreFactory.read.doc(id, options).get()).data();
},
findByIdOrThrow: async (id, options) => {
const reference = firestoreFactory.read.doc(id, options);
const doc = await reference.get();
if (!doc.exists) throw new Error(`Document ${reference.path} not found`);
return doc.data();
},
findByIdWithFallback: async (id, fallback) => {
const doc = await firestoreFactory.read.doc(id).get();
if (doc.exists) return doc.data();
if (schema.includeDocumentIdForZod) return schema.zod.parse({
_id: id,
...fallback
});
return {
_id: id,
...schema.zod.parse(fallback)
};
},
add: async (data) => firestoreFactory.write.collection().add(data),
create: async (id, data) => firestoreFactory.write.doc(id).create(data),
set: async (id, data, setOptions) => setOptions ? firestoreFactory.write.doc(id).set(data, setOptions) : firestoreFactory.write.doc(id).set(data),
update: (id, data, precondition) => precondition ? firestoreFactory.write.doc(id).update(data, precondition) : firestoreFactory.write.doc(id).update(data),
delete: (id, precondition) => firestoreFactory.write.doc(id).delete(precondition)
});
//#endregion
//#region src/collection/factory/single-document-collection-factory.ts
const singleDocumentCollectionFactory = (firestoreFactory, schema, singleDocumentKey) => {
const { read, write, findById, findByIdOrThrow, findByIdWithFallback, create, set, update, delete: deleteDocument, ...rest } = multiDocumentCollectionFactory(firestoreFactory, schema);
return {
...rest,
singleDocumentKey,
read: {
...read,
doc: (options) => read.doc(singleDocumentKey, options)
},
find: (options) => findById(singleDocumentKey, options),
findOrThrow: (options) => findByIdOrThrow(singleDocumentKey, options),
findWithFallback: (fallback) => findByIdWithFallback(singleDocumentKey, fallback),
write: {
...write,
doc: () => write.doc(singleDocumentKey)
},
create: (data) => create(singleDocumentKey, data),
set: (data, setOptions) => setOptions ? set(singleDocumentKey, data, setOptions) : set(singleDocumentKey, data),
update: (data, precondition) => update(singleDocumentKey, data, precondition),
delete: (precondition) => deleteDocument(singleDocumentKey, precondition)
};
};
//#endregion
//#region src/primitive/firestore-collection-path.ts
const firestoreCollectionPath = (path) => Array.isArray(path) ? path.join("/") : path;
//#endregion
//#region src/primitive/firestore-collection.ts
const firestoreCollection = (collectionPath, firestore = (0, firebase_admin_firestore.getFirestore)()) => firestore.collection(firestoreCollectionPath(collectionPath));
//#endregion
//#region src/primitive/firestore-collection-group.ts
const firestoreCollectionGroup = (collectionId, firestore = (0, firebase_admin_firestore.getFirestore)()) => firestore.collectionGroup(collectionId);
//#endregion
//#region src/primitive/firestore-document-path.ts
const firestoreDocumentPath = (collectionPath, documentId) => `${firestoreCollectionPath(collectionPath)}/${documentId}`;
//#endregion
//#region src/primitive/firestore-document.ts
const firestoreDocument = (collectionPath, documentId, firestore = (0, firebase_admin_firestore.getFirestore)()) => firestore.doc(firestoreDocumentPath(collectionPath, documentId));
//#endregion
//#region src/query/query-helper.ts
const queryHelper = (queryFactory) => ({
prepare: (query) => queryFactory(query),
query: (query) => queryFactory(query).get(),
count: async (query) => {
return (await queryFactory(query).count().get()).data().count;
},
findMany: async (query) => {
return (await queryFactory(query).get()).docs.map((doc) => doc.data());
},
findUnique: async (query) => {
const snapshot = await queryFactory(query).get();
if (snapshot.size > 1) throw new Error(`Query ${query.name} returned more than one document`);
if (snapshot.size === 0) return null;
return snapshot.docs[0].data();
},
findUniqueOrThrow: async (query) => {
const snapshot = await queryFactory(query).get();
if (snapshot.size > 1) throw new Error(`Query ${query.name} returned more than one document`);
if (snapshot.size === 0) throw new Error(`Query ${query.name} returned no documents`);
return snapshot.docs[0].data();
},
findFirst: async (query) => {
const snapshot = await queryFactory(query).get();
if (snapshot.size === 0) return null;
return snapshot.docs[0].data();
},
findFirstOrThrow: async (query) => {
const snapshot = await queryFactory(query).get();
if (snapshot.size === 0) throw new Error(`Query ${query.name} returned no documents`);
return snapshot.docs[0].data();
}
});
//#endregion
//#region src/query/query-specification.ts
const isWhereTuple = (filter) => Array.isArray(filter);
const applyQuerySpecification = (query, { where, orderBy, limit, limitToLast, offset, startAt, startAfter, endAt, endBefore }) => {
let result = query;
if (where) result = isWhereTuple(where) ? where.reduce((acc, [field, operator, value]) => acc.where(field, operator, value), result) : result.where(where);
if (orderBy) result = orderBy.reduce((acc, [field, direction]) => acc.orderBy(field, direction), result);
if (limit) result = result.limit(limit);
if (limitToLast) result = result.limitToLast(limitToLast);
if (offset) result = result.offset(offset);
if (startAt) result = Array.isArray(startAt) ? result.startAt(...startAt) : result.startAt(startAt);
if (startAfter) result = Array.isArray(startAfter) ? result.startAfter(...startAfter) : result.startAfter(startAfter);
if (endAt) result = Array.isArray(endAt) ? result.endAt(...endAt) : result.endAt(endAt);
if (endBefore) result = Array.isArray(endBefore) ? result.endBefore(...endBefore) : result.endBefore(endBefore);
return result;
};
//#endregion
//#region src/schema/schema-firestore-query-factory.ts
const schemaFirestoreQueryFactory = (queryBuilder, collectionName) => ({
collectionName,
prepare: (query, options) => applyQuerySpecification(queryBuilder(options), query),
query: (query, options) => applyQuerySpecification(queryBuilder(options), query).get(),
count: async (query) => {
return (await applyQuerySpecification(queryBuilder(), query).count().get()).data().count;
},
findMany: async (query, options) => {
return (await applyQuerySpecification(queryBuilder(options), query).get()).docs.map((doc) => doc.data());
},
findUnique: async (query, options) => {
const snapshot = await applyQuerySpecification(queryBuilder(options), query).get();
if (snapshot.size > 1) throw new Error(`Query ${query.name} returned more than one document`);
if (snapshot.size === 0) return null;
return snapshot.docs[0].data();
},
findUniqueOrThrow: async (query, options) => {
const snapshot = await applyQuerySpecification(queryBuilder(options), query).get();
if (snapshot.size > 1) throw new Error(`Query ${query.name} returned more than one document`);
if (snapshot.size === 0) throw new Error(`Query ${query.name} returned no documents`);
return snapshot.docs[0].data();
},
findFirst: async (query, options) => {
const snapshot = await applyQuerySpecification(queryBuilder(options), query).get();
if (snapshot.size === 0) return null;
return snapshot.docs[0].data();
},
findFirstOrThrow: async (query, options) => {
const snapshot = await applyQuerySpecification(queryBuilder(options), query).get();
if (snapshot.size === 0) throw new Error(`Query ${query.name} returned no documents`);
return snapshot.docs[0].data();
}
});
//#endregion
//#region src/zod-converters/firestore-omit-meta-data-converter.ts
const omitMetadata = ({ _id, _readTime, _createTime, _updateTime, ...rest }) => rest;
const firestoreOmitMetaDataConverter = () => ({
toFirestore: (modelObject) => omitMetadata(modelObject),
fromFirestore: (snapshot) => snapshot.data()
});
//#endregion
//#region src/zod-converters/firestore-zod-data-converter.ts
const firestoreZodDataConverter = (zod, outputOptions, options) => ({
...firestoreOmitMetaDataConverter(),
fromFirestore: (snapshot) => {
const data = options?.snapshotDataConverter ? options.snapshotDataConverter(snapshot) : snapshot.data();
const output = zod.safeParse(options?.includeDocumentIdForZod ? {
_id: snapshot.id,
...data
} : data);
if (!output.success) throw options?.zodErrorHandler ? options.zodErrorHandler(output.error, snapshot) : output.error;
return {
...outputOptions?._id !== false ? { _id: snapshot.id } : {},
...outputOptions?._createTime ? { _createTime: snapshot.createTime } : {},
...outputOptions?._updateTime ? { _updateTime: snapshot.updateTime } : {},
...outputOptions?._readTime ? { _readTime: snapshot.readTime } : {},
...output.data
};
}
});
//#endregion
//#region src/schema/schema-firestore-zod-data-converter-factory.ts
const schemaFirestoreZodDataConverter = ({ zod, includeDocumentIdForZod }, outputOptions, converterOptions) => firestoreZodDataConverter(zod, outputOptions, {
includeDocumentIdForZod,
...converterOptions
});
const schemaFirestoreZodDataConverterFactory = (schema, converterOptions) => {
const memoized = /* @__PURE__ */ new WeakMap();
const defaultFactory = schemaFirestoreZodDataConverter(schema, void 0, converterOptions);
return (outputOptions) => {
if (!outputOptions) return defaultFactory;
const memoizedConverter = memoized.get(outputOptions);
if (memoizedConverter) return memoizedConverter;
const converter = schemaFirestoreZodDataConverter(schema, outputOptions, converterOptions);
memoized.set(outputOptions, converter);
return converter;
};
};
//#endregion
//#region src/schema/schema-firestore-read-factory-builder.ts
const schemaFirestoreReadFactoryBuilder = (collectionName, schema, { getFirestore: getFirestore$3, ...converterOptions } = {}) => {
const zodConverterFactory = schemaFirestoreZodDataConverterFactory(schema, converterOptions);
const collectionGroup = (options) => firestoreCollectionGroup(collectionName, getFirestore$3?.()).withConverter(zodConverterFactory(options));
const build = (parentPath) => {
const collectionPath = parentPath ? [...parentPath, collectionName] : [collectionName];
return {
collection: (options) => firestoreCollection(collectionPath, getFirestore$3?.()).withConverter(zodConverterFactory(options)),
doc: (id, options) => firestoreDocument(collectionPath, id, getFirestore$3?.()).withConverter(zodConverterFactory(options)),
collectionGroup
};
};
return {
build,
zodConverter: zodConverterFactory,
group: schemaFirestoreQueryFactory(collectionGroup, collectionName)
};
};
//#endregion
//#region src/schema/schema-firestore-write-factory-builder.ts
const schemaFirestoreWriteFactoryBuilder = (collectionName, _schema, { getFirestore: getFirestore$3 } = {}) => {
const converter = firestoreOmitMetaDataConverter();
const build = (parentPath) => {
const collectionPath = parentPath ? [...parentPath, collectionName] : [collectionName];
return {
collection: () => firestoreCollection(collectionPath, getFirestore$3?.()).withConverter(converter),
doc: (id) => firestoreDocument(collectionPath, id, getFirestore$3?.()).withConverter(converter)
};
};
return { build };
};
//#endregion
//#region src/schema/schema-firestore-factory-builder.ts
const schemaFirestoreFactoryBuilder = (collectionName, schema, factoryOptions) => {
const readBuilder = schemaFirestoreReadFactoryBuilder(collectionName, schema, factoryOptions);
const writeBuilder = schemaFirestoreWriteFactoryBuilder(collectionName, schema, factoryOptions);
const build = (parentPath) => {
const read = readBuilder.build(parentPath);
const write = writeBuilder.build(parentPath);
return {
...schemaFirestoreQueryFactory(read.collection, collectionName),
read,
write
};
};
return {
...readBuilder,
...writeBuilder,
build
};
};
//#endregion
//#region src/collection/factory/collection-factory-builder.ts
const collectionFactoryBuilder = (collectionName, schema, options) => {
const firestoreFactoryBuilder = schemaFirestoreFactoryBuilder(collectionName, schema, options);
const build = (parentPath) => {
const firestoreFactory = firestoreFactoryBuilder.build(parentPath);
const collection = typeof schema.singleDocumentKey === "string" ? singleDocumentCollectionFactory(firestoreFactory, schema, schema.singleDocumentKey) : multiDocumentCollectionFactory(firestoreFactory, schema);
return {
collectionPath: parentPath ? firestoreCollectionPath([...parentPath, collectionName]) : collectionName,
...schema,
...collection,
collectionName
};
};
return {
...firestoreFactoryBuilder,
build
};
};
//#endregion
//#region src/collection/internal/internal-collections-builder.ts
const internalCollectionsBuilder = (internalSchema, parentPath) => Object.fromEntries(Object.entries(internalSchema).map(([collectionName, schemaBuilder]) => [collectionName, internalCollectionBuilder(schemaBuilder, parentPath)]));
const internalCollectionBuilder = (internalCollectionSchema$1, parentPath) => {
const collection = internalCollectionSchema$1.build(parentPath);
const { internalSubSchemas } = internalCollectionSchema$1;
if (Object.keys(internalSubSchemas).length === 0) return collection;
const subCollectionsAccessor = (documentId) => internalCollectionsBuilder(internalSubSchemas, [collection.collectionPath, documentId]);
return Object.assign(subCollectionsAccessor, collection, internalSubSchemas);
};
//#endregion
//#region src/collection/internal/internal-schema-builder.ts
const internalSchemaBuilder = (schema, options) => Object.fromEntries(Object.entries(schema).map(([collectionName, collectionSchema]) => [collectionName, internalCollectionSchema(collectionName, collectionSchema, options)]));
const internalCollectionSchema = (collectionName, collectionSchema, options) => {
const factoryBuilder = collectionFactoryBuilder(collectionName, collectionSchema, options);
const { zod, singleDocumentKey, includeDocumentIdForZod, readonlyDocuments, ...subSchemas } = collectionSchema;
const internalSubSchemas = Object.keys(subSchemas).length === 0 ? subSchemas : internalSchemaBuilder(subSchemas, options);
return {
collectionName,
zod,
singleDocumentKey,
includeDocumentIdForZod,
readonlyDocuments,
internalSubSchemas,
...internalSubSchemas,
...factoryBuilder
};
};
//#endregion
//#region src/collection/sub-collections-schema.ts
const subCollectionsSchema = (collectionSchema) => {
const { zod, singleDocumentKey, includeDocumentIdForZod, readonlyDocuments, ...rest } = collectionSchema;
return rest;
};
//#endregion
//#region src/collection/root-collections-builder.ts
const rootCollectionsBuilder = (schema, factoryOptions) => Object.fromEntries(Object.entries(schema).map(([collectionName, collectionSchema]) => [collectionName, rootCollectionBuilder(collectionName, collectionSchema, factoryOptions)]));
const rootCollectionBuilder = (collectionName, collectionSchema, factoryOptions) => {
const collectionFactory = collectionFactoryBuilder(collectionName, collectionSchema, factoryOptions).build();
const subSchemas = subCollectionsSchema(collectionSchema);
if (Object.keys(subSchemas).length === 0) return collectionFactory;
const internalSchema = internalSchemaBuilder(subSchemas, factoryOptions);
const subCollectionsAccessor = (documentId) => internalCollectionsBuilder(internalSchema, [collectionFactory.collectionPath, documentId]);
return Object.assign(subCollectionsAccessor, collectionFactory, internalSchema);
};
//#endregion
//#region src/collection-builder.ts
/**
* Build collections from a schema
* @param schema
* @param options
*/
const collectionsBuilder = (schema, options) => rootCollectionsBuilder(schema, options);
//#endregion
exports.applyQuerySpecification = applyQuerySpecification;
exports.collectionsBuilder = collectionsBuilder;
exports.firestoreCollection = firestoreCollection;
exports.firestoreCollectionGroup = firestoreCollectionGroup;
exports.firestoreCollectionPath = firestoreCollectionPath;
exports.firestoreDocument = firestoreDocument;
exports.firestoreDocumentPath = firestoreDocumentPath;
exports.firestoreOmitMetaDataConverter = firestoreOmitMetaDataConverter;
exports.firestoreZodDataConverter = firestoreZodDataConverter;
exports.multiDocumentCollectionFactory = multiDocumentCollectionFactory;
exports.queryHelper = queryHelper;
exports.singleDocumentCollectionFactory = singleDocumentCollectionFactory;