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
JavaScript
;
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