UNPKG

fireodm

Version:

A basic and extensible ODM for the Firestore Admin SDK in Node.js with decorators, relationships, and validation.

261 lines 9.97 kB
import * as admin from "firebase-admin"; import { GeoPoint as FireGeoPoint, Timestamp as FireTimestamp, } from "firebase-admin/firestore"; import "reflect-metadata"; import { z } from "zod"; import { BaseModel } from "./base-model"; import { Validate } from "./validation"; var DocumentReference = admin.firestore.DocumentReference; export const SUBCOL_DOC_KEY = Symbol("subcollectionDocs"); export const COLLECTION_KEY = Symbol("collectionName"); export const RELATION_KEY = Symbol("relations"); export const TIMESTAMP_KEY = Symbol("timestamps"); export const BOOLEAN_KEY = Symbol("booleans"); export const SUBCOL_KEY = Symbol("subcollections"); export const SUBMODEL_KEY = Symbol("subcollectionModel"); /** * Class decorator to define the Firestore collection name for a model. * @param name The name of the Firestore collection. * @example * ```typescript * @Collection('users') * class User extends BaseModel { * // ... * } * ``` */ export function Collection(name) { return function (constructor) { if (!name || typeof name !== "string") { throw new Error(`@Collection decorator requires a non-empty string argument. Received: ${name}`); } Reflect.defineMetadata(COLLECTION_KEY, name, constructor); }; } /** * Decorator to link a property to a specific document within a subcollection. * @param modelGetter A function returning the constructor of the model for the document. * @param docId The fixed ID of the document within the subcollection. * @param options Options object, must include the subcollection name. */ export function SubCollectionDoc(modelGetter, docId, options) { return (target, propertyName) => { const ctor = target.constructor; const list = Reflect.getOwnMetadata(SUBCOL_DOC_KEY, ctor) || []; if (!options || !options.subcollection) { throw new Error(`@SubCollectionDoc for "${propertyName}" requires a 'subcollection' name in options.`); } list.push({ propertyName, docId, subcollectionName: options.subcollection, model: modelGetter, }); Reflect.defineMetadata(SUBCOL_DOC_KEY, list, ctor); }; } export function SubCollectionModel(parentGetter, subPath) { return (target) => { // target here is the constructor function of GiftCard (whatever its signature) Reflect.defineMetadata(SUBMODEL_KEY, { parentModel: parentGetter, subPath }, target); }; } export function SubCollection(modelGetter, name) { return (target, propertyName) => { const ctor = target.constructor; const list = Reflect.getOwnMetadata(SUBCOL_KEY, ctor) || []; list.push({ propertyName, name: name !== null && name !== void 0 ? name : propertyName, model: modelGetter, }); Reflect.defineMetadata(SUBCOL_KEY, list, ctor); }; } /** * Property decorator to define a relationship stored as a DocumentReference. * @param relatedModelGetter A function returning the constructor of the related model (e.g., `() => User`). Essential for handling circular dependencies. * @example * ```typescript * import { Department } from './Department'; // Assuming Department model exists * * @Collection('employees') * class Employee extends BaseModel { * name: string; * * @Relation(() => Department) * department?: DocumentReference | Department | null; // Can hold Ref, populated instance, or be null * } * ``` */ export function Relation(relatedModelGetter, options = { lazy: true }) { if (typeof relatedModelGetter !== "function") { throw new Error("@Relation decorator requires a function argument that returns the related model constructor."); } return function (target, propertyName) { var _a; const relations = Reflect.getOwnMetadata(RELATION_KEY, target.constructor) || []; if (relations.some((r) => r.propertyName === propertyName)) { const idx = relations.findIndex((r) => r.propertyName === propertyName); relations.splice(idx, 1); } relations.push({ propertyName, relatedModel: relatedModelGetter, lazy: (_a = options.lazy) !== null && _a !== void 0 ? _a : true, }); Reflect.defineMetadata(RELATION_KEY, relations, target.constructor); }; } // --- Metadata Accessor Functions (Internal) --- /** @internal Gets the collection name from metadata, searching prototype chain. */ export function getCollectionName(target) { let current = target; while (current && current !== Object.prototype) { const top = Reflect.getOwnMetadata(COLLECTION_KEY, current); if (typeof top === 'string') { return top; } const sub = Reflect.getOwnMetadata(SUBMODEL_KEY, current); if (sub) { return sub.subPath; } current = Object.getPrototypeOf(current); } return undefined; } /** @internal Gets relation metadata, searching and merging from prototype chain. */ export function getRelationMetadata(target) { let relations = []; let currentTarget = target; const definedProperties = new Set(); // Keep track of properties defined lower down while (currentTarget && currentTarget !== Object.prototype) { const currentRelations = Reflect.getOwnMetadata(RELATION_KEY, currentTarget); if (currentRelations) { currentRelations.forEach((rel) => { // Add relation only if not already defined by a subclass (higher in chain) if (!definedProperties.has(rel.propertyName)) { relations.push(rel); definedProperties.add(rel.propertyName); } }); } currentTarget = Object.getPrototypeOf(currentTarget); } // Return relations collected from prototype chain (subclass definitions take precedence) return relations; } export function StringField(opts = { required: false, }) { let schema = z.string(); if (opts.min != null) schema = schema.min(opts.min, { message: opts.message }); if (opts.max != null) schema = schema.max(opts.max, { message: opts.message }); if (!opts.required) schema = schema.optional(); return Validate(schema); } export function EmailField(message = "Invalid email", opts = { required: false }) { let schema = z.string().email({ message }); if (!opts.required) schema = schema.optional(); return Validate(schema); } export function NumberField(opts = { required: false, }) { let schema = z.number(); if (opts.min != null) schema = schema.min(opts.min, { message: opts.message }); if (opts.max != null) schema = schema.max(opts.max, { message: opts.message }); if (!opts.required) schema = schema.optional(); return Validate(schema); } export function BooleanField(opts = { required: false }) { // Build Zod schema for boolean let schema = z.boolean(); if (!opts.required) schema = schema.optional(); return function (target, propertyName) { // Apply validation Validate(schema)(target, propertyName); // Apply defaultNow if requested if (opts.defaultValue) { const existing = Reflect.getOwnMetadata(BOOLEAN_KEY, target.constructor) || []; existing.push({ prop: propertyName, defaultValue: opts.defaultValue }); Reflect.defineMetadata(BOOLEAN_KEY, existing, target.constructor); } }; } export function TimestampField(opts = { required: false, autoFill: false, }) { // Build Zod schema let schema = z.instanceof(FireTimestamp); if (!opts.required) schema = schema.optional(); // Decorator combining validation and autoFill metadata return function (target, propertyName) { // Apply validation Validate(schema)(target, propertyName); // Apply defaultNow if requested if (opts.autoFill) { const existing = Reflect.getOwnMetadata(TIMESTAMP_KEY, target.constructor) || []; existing.push(propertyName); Reflect.defineMetadata(TIMESTAMP_KEY, existing, target.constructor); } }; } export function GeoPointField(opts = { required: false }) { let schema = z.instanceof(FireGeoPoint); if (!opts.required) schema = schema.optional(); return Validate(schema); } export function ArrayField(schemaDef, opts = { required: false }) { let arrSchema = z.array(schemaDef); if (!opts.required) arrSchema = arrSchema.optional(); return Validate(arrSchema); } export function MapField(schemaDef, opts = { required: false }) { let mapSchema = z.record(z.string(), schemaDef); if (!opts.required) mapSchema = mapSchema.optional(); return Validate(mapSchema); } /** * Decorator for Firestore DocumentReference fields. * Combines validation with loading logic: apply alongside @Relation for relations. */ // Workaround for Firestore's private constructor on DocumentReference const FireDocRefCtor = DocumentReference; /** * Decorator for Firestore DocumentReference fields. * Combines validation with loading logic: apply alongside @Relation for relations. */ export function DocumentReferenceField(opts = { required: false }) { let schema = z.union([ z.instanceof(FireDocRefCtor), z.instanceof(BaseModel), z.null(), ]); if (!opts.required) { schema = schema.optional(); } return Validate(schema); } export function EnumField(enumObj, opts = { required: false }) { let enumSchema = z.nativeEnum(enumObj); if (!opts.required) enumSchema = enumSchema.optional(); if (opts.defaultValue !== undefined) { enumSchema = enumSchema.default(opts.defaultValue); } return Validate(enumSchema); } //# sourceMappingURL=decorators.js.map