UNPKG

zod-firebase-admin

Version:
360 lines (337 loc) 16.7 kB
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;