zod-firebase-admin
Version:
zod firebase-admin schema
405 lines (380 loc) • 17.4 kB
JavaScript
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;
;