UNPKG

fireodm

Version:

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

314 lines 12.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SUBMODEL_KEY = exports.SUBCOL_KEY = exports.BOOLEAN_KEY = exports.TIMESTAMP_KEY = exports.RELATION_KEY = exports.COLLECTION_KEY = exports.SUBCOL_DOC_KEY = void 0; exports.Collection = Collection; exports.SubCollectionDoc = SubCollectionDoc; exports.SubCollectionModel = SubCollectionModel; exports.SubCollection = SubCollection; exports.Relation = Relation; exports.getCollectionName = getCollectionName; exports.getRelationMetadata = getRelationMetadata; exports.StringField = StringField; exports.EmailField = EmailField; exports.NumberField = NumberField; exports.BooleanField = BooleanField; exports.TimestampField = TimestampField; exports.GeoPointField = GeoPointField; exports.ArrayField = ArrayField; exports.MapField = MapField; exports.DocumentReferenceField = DocumentReferenceField; exports.EnumField = EnumField; const admin = __importStar(require("firebase-admin")); const firestore_1 = require("firebase-admin/firestore"); require("reflect-metadata"); const zod_1 = require("zod"); const base_model_1 = require("./base-model"); const validation_1 = require("./validation"); var DocumentReference = admin.firestore.DocumentReference; exports.SUBCOL_DOC_KEY = Symbol("subcollectionDocs"); exports.COLLECTION_KEY = Symbol("collectionName"); exports.RELATION_KEY = Symbol("relations"); exports.TIMESTAMP_KEY = Symbol("timestamps"); exports.BOOLEAN_KEY = Symbol("booleans"); exports.SUBCOL_KEY = Symbol("subcollections"); exports.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 { * // ... * } * ``` */ 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(exports.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. */ function SubCollectionDoc(modelGetter, docId, options) { return (target, propertyName) => { const ctor = target.constructor; const list = Reflect.getOwnMetadata(exports.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(exports.SUBCOL_DOC_KEY, list, ctor); }; } function SubCollectionModel(parentGetter, subPath) { return (target) => { // target here is the constructor function of GiftCard (whatever its signature) Reflect.defineMetadata(exports.SUBMODEL_KEY, { parentModel: parentGetter, subPath }, target); }; } function SubCollection(modelGetter, name) { return (target, propertyName) => { const ctor = target.constructor; const list = Reflect.getOwnMetadata(exports.SUBCOL_KEY, ctor) || []; list.push({ propertyName, name: name !== null && name !== void 0 ? name : propertyName, model: modelGetter, }); Reflect.defineMetadata(exports.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 * } * ``` */ 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(exports.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(exports.RELATION_KEY, relations, target.constructor); }; } // --- Metadata Accessor Functions (Internal) --- /** @internal Gets the collection name from metadata, searching prototype chain. */ function getCollectionName(target) { let current = target; while (current && current !== Object.prototype) { const top = Reflect.getOwnMetadata(exports.COLLECTION_KEY, current); if (typeof top === 'string') { return top; } const sub = Reflect.getOwnMetadata(exports.SUBMODEL_KEY, current); if (sub) { return sub.subPath; } current = Object.getPrototypeOf(current); } return undefined; } /** @internal Gets relation metadata, searching and merging from prototype chain. */ 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(exports.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; } function StringField(opts = { required: false, }) { let schema = zod_1.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 (0, validation_1.Validate)(schema); } function EmailField(message = "Invalid email", opts = { required: false }) { let schema = zod_1.z.string().email({ message }); if (!opts.required) schema = schema.optional(); return (0, validation_1.Validate)(schema); } function NumberField(opts = { required: false, }) { let schema = zod_1.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 (0, validation_1.Validate)(schema); } function BooleanField(opts = { required: false }) { // Build Zod schema for boolean let schema = zod_1.z.boolean(); if (!opts.required) schema = schema.optional(); return function (target, propertyName) { // Apply validation (0, validation_1.Validate)(schema)(target, propertyName); // Apply defaultNow if requested if (opts.defaultValue) { const existing = Reflect.getOwnMetadata(exports.BOOLEAN_KEY, target.constructor) || []; existing.push({ prop: propertyName, defaultValue: opts.defaultValue }); Reflect.defineMetadata(exports.BOOLEAN_KEY, existing, target.constructor); } }; } function TimestampField(opts = { required: false, autoFill: false, }) { // Build Zod schema let schema = zod_1.z.instanceof(firestore_1.Timestamp); if (!opts.required) schema = schema.optional(); // Decorator combining validation and autoFill metadata return function (target, propertyName) { // Apply validation (0, validation_1.Validate)(schema)(target, propertyName); // Apply defaultNow if requested if (opts.autoFill) { const existing = Reflect.getOwnMetadata(exports.TIMESTAMP_KEY, target.constructor) || []; existing.push(propertyName); Reflect.defineMetadata(exports.TIMESTAMP_KEY, existing, target.constructor); } }; } function GeoPointField(opts = { required: false }) { let schema = zod_1.z.instanceof(firestore_1.GeoPoint); if (!opts.required) schema = schema.optional(); return (0, validation_1.Validate)(schema); } function ArrayField(schemaDef, opts = { required: false }) { let arrSchema = zod_1.z.array(schemaDef); if (!opts.required) arrSchema = arrSchema.optional(); return (0, validation_1.Validate)(arrSchema); } function MapField(schemaDef, opts = { required: false }) { let mapSchema = zod_1.z.record(zod_1.z.string(), schemaDef); if (!opts.required) mapSchema = mapSchema.optional(); return (0, validation_1.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. */ function DocumentReferenceField(opts = { required: false }) { let schema = zod_1.z.union([ zod_1.z.instanceof(FireDocRefCtor), zod_1.z.instanceof(base_model_1.BaseModel), zod_1.z.null(), ]); if (!opts.required) { schema = schema.optional(); } return (0, validation_1.Validate)(schema); } function EnumField(enumObj, opts = { required: false }) { let enumSchema = zod_1.z.nativeEnum(enumObj); if (!opts.required) enumSchema = enumSchema.optional(); if (opts.defaultValue !== undefined) { enumSchema = enumSchema.default(opts.defaultValue); } return (0, validation_1.Validate)(enumSchema); } //# sourceMappingURL=decorators.js.map