UNPKG

zod-firebase-admin

Version:
405 lines (380 loc) 17.4 kB
'use strict'; var firestore = require('firebase-admin/firestore'); const firestoreCollectionPath = (path) => Array.isArray(path) ? path.join('/') : path; const firestoreCollection = (collectionPath, firestore$1 = firestore.getFirestore()) => firestore$1.collection(firestoreCollectionPath(collectionPath)); const firestoreCollectionGroup = (collectionId, firestore$1 = firestore.getFirestore()) => firestore$1.collectionGroup(collectionId); const firestoreDocumentPath = (collectionPath, documentId) => `${firestoreCollectionPath(collectionPath)}/${documentId}`; const firestoreDocument = (collectionPath, documentId, firestore$1 = firestore.getFirestore()) => firestore$1.doc(firestoreDocumentPath(collectionPath, documentId)); const omitMetadata = ({ _id, _readTime, _createTime, _updateTime, ...rest }) => rest; const firestoreOmitMetaDataConverter = () => ({ toFirestore: (modelObject) => omitMetadata(modelObject), fromFirestore: (snapshot) => snapshot.data(), }); const firestoreZodDataConverter = (zod, outputOptions, options) => ({ toFirestore: (modelObject) => omitMetadata(modelObject), fromFirestore: (snapshot) => { const data = options?.snapshotDataConverter ? options.snapshotDataConverter(snapshot) : snapshot.data(); const output = zod.safeParse(options?.includeDocumentIdForZod ? { // _id can be used in the zod parse for discriminatedUnion _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, }; }, }); const multiDocumentCollectionFactory = (firestoreFactory, schema) => ({ ...firestoreFactory, findById: async (id, options) => { const doc = await firestoreFactory.read.doc(id, options).get(); return doc.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`); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return doc.data(); }, findByIdWithFallback: async (id, fallback) => { const doc = await firestoreFactory.read.doc(id).get(); if (doc.exists) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 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), }); 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), }; }; const queryHelper = (queryFactory) => ({ prepare: (query) => queryFactory(query), query: (query) => queryFactory(query).get(), count: async (query) => { const snapshot = await queryFactory(query).count().get(); return snapshot.data().count; }, findMany: async (query) => { const snapshot = await queryFactory(query).get(); return snapshot.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(); }, }); 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; }; const schemaFirestoreQueryFactory = (queryBuilder, collectionName) => ({ collectionName, prepare: (query, options) => applyQuerySpecification(queryBuilder(options), query), query: (query, options) => applyQuerySpecification(queryBuilder(options), query).get(), count: async (query) => { const snapshot = await applyQuerySpecification(queryBuilder(), query).count().get(); return snapshot.data().count; }, findMany: async (query, options) => { const snapshot = await applyQuerySpecification(queryBuilder(options), query).get(); return snapshot.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(); }, }); const schemaFirestoreZodDataConverter = ({ zod, includeDocumentIdForZod }, outputOptions, converterOptions) => firestoreZodDataConverter(zod, outputOptions, { includeDocumentIdForZod, ...converterOptions }); const schemaFirestoreZodDataConverterFactory = (schema, converterOptions) => { const memoized = new WeakMap(); const defaultFactory = schemaFirestoreZodDataConverter(schema, undefined, 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; }; }; const schemaFirestoreReadFactoryBuilder = (collectionName, schema, { getFirestore, ...converterOptions } = {}) => { const zodConverterFactory = schemaFirestoreZodDataConverterFactory(schema, converterOptions); const collectionGroup = (options) => firestoreCollectionGroup(collectionName, getFirestore?.()).withConverter(zodConverterFactory(options)); const build = (parentPath) => { const collectionPath = parentPath ? [...parentPath, collectionName] : [collectionName]; return { collection: (options) => firestoreCollection(collectionPath, getFirestore?.()).withConverter(zodConverterFactory(options)), doc: (id, options) => firestoreDocument(collectionPath, id, getFirestore?.()).withConverter(zodConverterFactory(options)), collectionGroup, }; }; return { build, zodConverter: zodConverterFactory, group: schemaFirestoreQueryFactory(collectionGroup, collectionName), }; }; const schemaFirestoreWriteFactoryBuilder = (collectionName, _schema, { getFirestore } = {}) => { const converter = firestoreOmitMetaDataConverter(); const build = (parentPath) => { const collectionPath = parentPath ? [...parentPath, collectionName] : [collectionName]; return { collection: () => firestoreCollection(collectionPath, getFirestore?.()).withConverter(converter), doc: (id) => firestoreDocument(collectionPath, id, getFirestore?.()).withConverter(converter), }; }; return { build, }; }; 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, }; }; 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, }; }; const internalCollectionsBuilder = (internalSchema, parentPath) => Object.fromEntries(Object.entries(internalSchema).map(([collectionName, schemaBuilder]) => [ collectionName, internalCollectionBuilder(schemaBuilder, parentPath), ])); const internalCollectionBuilder = (internalCollectionSchema, parentPath) => { const collection = internalCollectionSchema.build(parentPath); const { internalSubSchema } = internalCollectionSchema; if (!internalSubSchema) { return collection; } const subCollectionsAccessor = (documentId) => internalCollectionsBuilder(internalSubSchema, [collection.collectionPath, documentId]); return Object.assign(subCollectionsAccessor, collection, internalSubSchema); }; 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, ...subSchema } = collectionSchema; const internalSubSchema = Object.keys(subSchema).length === 0 ? null : internalSchemaBuilder(subSchema, options); return { collectionName, zod, singleDocumentKey, includeDocumentIdForZod, readonlyDocuments, internalSubSchema, ...internalSubSchema, ...factoryBuilder, }; }; const subCollectionsSchema = (collectionSchema) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { zod, singleDocumentKey, includeDocumentIdForZod, readonlyDocuments, ...rest } = collectionSchema; return rest; }; const rootCollectionsBuilder = (schema, factoryOptions) => Object.fromEntries(Object.entries(schema).map(([collectionName, collectionSchema]) => [ collectionName, rootCollectionBuilder(collectionName, collectionSchema, factoryOptions), ])); const rootCollectionBuilder = (collectionName, collectionSchema, factoryOptions) => { const builder = collectionFactoryBuilder(collectionName, collectionSchema, factoryOptions); const collectionFactory = builder.build(); const subSchema = subCollectionsSchema(collectionSchema); if (Object.keys(subSchema).length === 0) { return collectionFactory; } const internalSchema = internalSchemaBuilder(subSchema, factoryOptions); const subCollectionsAccessor = (documentId) => internalCollectionsBuilder(internalSchema, [collectionFactory.collectionPath, documentId]); return Object.assign(subCollectionsAccessor, collectionFactory, internalSchema); }; /** * Build collections from a schema * @param schema * @param options */ const collectionsBuilder = (schema, options) => rootCollectionsBuilder(schema, options); exports.applyQuerySpecification = applyQuerySpecification; exports.collectionsBuilder = collectionsBuilder; exports.firestoreCollection = firestoreCollection; exports.firestoreCollectionGroup = firestoreCollectionGroup; exports.firestoreCollectionPath = firestoreCollectionPath; exports.firestoreDocument = firestoreDocument; exports.firestoreDocumentPath = firestoreDocumentPath; exports.firestoreZodDataConverter = firestoreZodDataConverter; exports.multiDocumentCollectionFactory = multiDocumentCollectionFactory; exports.queryHelper = queryHelper; exports.singleDocumentCollectionFactory = singleDocumentCollectionFactory;