@fyrestack/database 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1229 @@
1
+
2
+ //#region src/context.ts
3
+ /**
4
+ * Detect if we're running in Node.js environment
5
+ */
6
+ const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
7
+ /**
8
+ * Browser shim for AsyncLocalStorage.
9
+ * Note: This implementation has limited support for parallel async operations.
10
+ * In browsers, parallel context isolation may not work correctly.
11
+ * For full isolation, use Node.js with native AsyncLocalStorage.
12
+ */
13
+ var AsyncLocalStorageShim = class {
14
+ constructor() {
15
+ this._store = void 0;
16
+ this._stack = [];
17
+ }
18
+ run(store, callback) {
19
+ this._stack.push(this._store);
20
+ this._store = store;
21
+ try {
22
+ const result = callback();
23
+ if (result && typeof result.then === "function") return result.finally(() => {
24
+ this._store = this._stack.pop();
25
+ });
26
+ this._store = this._stack.pop();
27
+ return result;
28
+ } catch (error) {
29
+ this._store = this._stack.pop();
30
+ throw error;
31
+ }
32
+ }
33
+ getStore() {
34
+ return this._store;
35
+ }
36
+ };
37
+ /**
38
+ * Get the AsyncLocalStorage implementation.
39
+ * Uses native AsyncLocalStorage in Node.js, falls back to shim in browsers.
40
+ */
41
+ function getAsyncLocalStorageClass() {
42
+ if (isNode) try {
43
+ const dynamicRequire = eval("typeof require !== \"undefined\" ? require : undefined");
44
+ if (dynamicRequire) return dynamicRequire("async_hooks").AsyncLocalStorage;
45
+ } catch {}
46
+ return AsyncLocalStorageShim;
47
+ }
48
+ /**
49
+ * The async context storage that holds the current operation context.
50
+ * This propagates automatically through async/await chains.
51
+ */
52
+ const AsyncLocalStorageClass = getAsyncLocalStorageClass();
53
+ const operationContext = new AsyncLocalStorageClass();
54
+ /**
55
+ * Get the current operation context, if any.
56
+ * Returns undefined when not inside a transaction or batch.
57
+ */
58
+ function getCurrentContext() {
59
+ return operationContext.getStore();
60
+ }
61
+ /**
62
+ * Check if we're currently inside a transaction.
63
+ */
64
+ function isInTransaction() {
65
+ return operationContext.getStore()?.type === "transaction";
66
+ }
67
+ /**
68
+ * Check if we're currently inside a batch.
69
+ */
70
+ function isInBatch() {
71
+ return operationContext.getStore()?.type === "batch";
72
+ }
73
+ /**
74
+ * Get the current transaction, if inside one.
75
+ * Returns undefined when not in a transaction.
76
+ */
77
+ function getCurrentTransaction() {
78
+ const ctx = operationContext.getStore();
79
+ return ctx?.type === "transaction" ? ctx.tx : void 0;
80
+ }
81
+ /**
82
+ * Get the current batch, if inside one.
83
+ * Returns undefined when not in a batch.
84
+ */
85
+ function getCurrentBatch() {
86
+ const ctx = operationContext.getStore();
87
+ return ctx?.type === "batch" ? ctx.batch : void 0;
88
+ }
89
+ /**
90
+ * Run a function within a transaction context.
91
+ * @internal Used by runTransaction()
92
+ */
93
+ function runWithTransaction(tx, fn) {
94
+ const ctx = {
95
+ type: "transaction",
96
+ tx
97
+ };
98
+ return operationContext.run(ctx, fn);
99
+ }
100
+ /**
101
+ * Run a function within a batch context.
102
+ * @internal Used by writeBatch()
103
+ */
104
+ function runWithBatch(batch, fn) {
105
+ const ctx = {
106
+ type: "batch",
107
+ batch
108
+ };
109
+ return operationContext.run(ctx, fn);
110
+ }
111
+ /**
112
+ * Type guard for transaction context.
113
+ */
114
+ function isTransactionContext(ctx) {
115
+ return ctx?.type === "transaction";
116
+ }
117
+ /**
118
+ * Type guard for batch context.
119
+ */
120
+ function isBatchContext(ctx) {
121
+ return ctx?.type === "batch";
122
+ }
123
+
124
+ //#endregion
125
+ //#region src/errors.ts
126
+ /**
127
+ * FyreStack Custom Error Types
128
+ *
129
+ * Provides typed errors for better error handling and debugging.
130
+ */
131
+ /**
132
+ * Base error class for all FyreStack errors.
133
+ */
134
+ var FyreStackError = class extends Error {
135
+ constructor(message, code) {
136
+ super(message);
137
+ this.name = "FyreStackError";
138
+ this.code = code;
139
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
140
+ }
141
+ };
142
+ /**
143
+ * Error thrown when validation fails.
144
+ */
145
+ var ValidationError = class ValidationError extends FyreStackError {
146
+ constructor(message, issues = []) {
147
+ super(message, "VALIDATION_ERROR");
148
+ this.name = "ValidationError";
149
+ this.issues = issues;
150
+ }
151
+ /**
152
+ * Create a ValidationError from Standard Schema validation issues.
153
+ */
154
+ static fromStandardSchema(issues) {
155
+ const mappedIssues = issues.map((issue) => ({
156
+ path: issue.path?.map((p) => typeof p === "object" ? String(p.key) : String(p)) ?? [],
157
+ message: issue.message
158
+ }));
159
+ return new ValidationError(mappedIssues.length > 0 && mappedIssues[0] !== void 0 ? mappedIssues.length === 1 ? mappedIssues[0].message : `Validation failed with ${String(mappedIssues.length)} issues` : "Validation failed", mappedIssues);
160
+ }
161
+ };
162
+ /**
163
+ * Error thrown when path parameters are invalid.
164
+ */
165
+ var PathParamError = class extends FyreStackError {
166
+ constructor(paramName, paramValue, reason) {
167
+ super(`Invalid path parameter "${paramName}": ${reason}`, "PATH_PARAM_ERROR");
168
+ this.name = "PathParamError";
169
+ this.paramName = paramName;
170
+ this.paramValue = paramValue;
171
+ }
172
+ };
173
+ /**
174
+ * Error thrown when Firestore adapter is not initialized.
175
+ */
176
+ var AdapterNotInitializedError = class extends FyreStackError {
177
+ constructor() {
178
+ super("Firestore adapter not initialized. Call setFirestoreAdapter() before using repositories.", "ADAPTER_NOT_INITIALIZED");
179
+ this.name = "AdapterNotInitializedError";
180
+ }
181
+ };
182
+ /**
183
+ * Error thrown when a model is missing required properties.
184
+ */
185
+ var ModelError = class extends FyreStackError {
186
+ constructor(message) {
187
+ super(message, "MODEL_ERROR");
188
+ this.name = "ModelError";
189
+ }
190
+ };
191
+ /**
192
+ * Error thrown when a document is not found.
193
+ */
194
+ var NotFoundError = class extends FyreStackError {
195
+ constructor(documentPath) {
196
+ super(`Document not found: ${documentPath}`, "NOT_FOUND");
197
+ this.name = "NotFoundError";
198
+ this.documentPath = documentPath;
199
+ }
200
+ };
201
+ /**
202
+ * Error thrown when a Firestore operation fails.
203
+ */
204
+ var FirestoreError = class FirestoreError extends FyreStackError {
205
+ constructor(message, originalError) {
206
+ super(message, "FIRESTORE_ERROR");
207
+ this.name = "FirestoreError";
208
+ this.originalError = originalError;
209
+ }
210
+ /**
211
+ * Wrap a Firestore error with context.
212
+ */
213
+ static wrap(operation, error) {
214
+ return new FirestoreError(`Firestore ${operation} failed: ${error instanceof Error ? error.message : "Unknown error"}`, error);
215
+ }
216
+ };
217
+ /**
218
+ * Helper to check if a ValidationResult is valid.
219
+ */
220
+ function isValidResult(result) {
221
+ return result.valid;
222
+ }
223
+
224
+ //#endregion
225
+ //#region src/firestore.types.ts
226
+ /**
227
+ * Abstract Firestore types for dependency injection.
228
+ * Allows switching between firebase-admin and client-side firebase.
229
+ */
230
+ /**
231
+ * Type guard to check if a value is a ServerTimestampSentinel.
232
+ * Note: This checks the runtime structure, not the brand.
233
+ */
234
+ function isServerTimestamp(value) {
235
+ return value !== null && typeof value === "object" && "__type__" in value && value.__type__ === "serverTimestamp";
236
+ }
237
+ /**
238
+ * Global Firestore adapter instance holder
239
+ */
240
+ let firestoreAdapter = null;
241
+ /**
242
+ * Set the Firestore adapter to use (call once at app initialization)
243
+ */
244
+ function setFirestoreAdapter(adapter) {
245
+ firestoreAdapter = adapter;
246
+ }
247
+ /**
248
+ * Get the current Firestore adapter
249
+ * @throws AdapterNotInitializedError if adapter not initialized
250
+ */
251
+ function getFirestoreAdapter() {
252
+ if (!firestoreAdapter) throw new AdapterNotInitializedError();
253
+ return firestoreAdapter;
254
+ }
255
+ /**
256
+ * Reset the Firestore adapter (for testing only)
257
+ * @internal
258
+ */
259
+ function _resetFirestoreAdapter() {
260
+ firestoreAdapter = null;
261
+ }
262
+
263
+ //#endregion
264
+ //#region src/batch.ts
265
+ /**
266
+ * Batch Write Support for FyreStack Database
267
+ *
268
+ * Provides atomic write-only operations across multiple repositories.
269
+ * Context is automatically propagated using AsyncContext.
270
+ */
271
+ /**
272
+ * Maximum operations per Firestore batch.
273
+ */
274
+ const MAX_BATCH_SIZE = 500;
275
+ /**
276
+ * Run operations within a write batch.
277
+ *
278
+ * Batches provide:
279
+ * - Atomic writes (all succeed or all fail)
280
+ * - Better performance than individual writes
281
+ * - Up to 500 operations per batch
282
+ * - Works across multiple repositories
283
+ *
284
+ * The batch context is **automatically propagated** to all repository
285
+ * methods called within the batch function - no need to pass context explicitly.
286
+ *
287
+ * Note: Batches are write-only. Use `runTransaction` if you need reads.
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * // Bulk create users
292
+ * await writeBatch(async () => {
293
+ * for (const user of users) {
294
+ * await UserRepo.save({}, user);
295
+ * }
296
+ * });
297
+ * ```
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * // Cross-repository batch write
302
+ * await writeBatch(async () => {
303
+ * await UserRepo.save({}, user);
304
+ * await ProfileRepo.save({ userId: user.id }, profile);
305
+ * await AuditRepo.save({}, auditLog);
306
+ * });
307
+ * ```
308
+ *
309
+ * @param fn - Async function to run within the batch
310
+ */
311
+ async function writeBatch(fn) {
312
+ const batch = getFirestoreAdapter().batch();
313
+ await runWithBatch(batch, fn);
314
+ await batch.commit();
315
+ }
316
+ /**
317
+ * Run operations with automatic batch chunking for large datasets.
318
+ *
319
+ * Automatically commits and creates new batches when exceeding 500 operations.
320
+ * Useful for migrations or bulk imports with thousands of records.
321
+ *
322
+ * The batch context is **automatically propagated** to all repository
323
+ * methods called within the function.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * // Process 10,000 records with automatic batching
328
+ * await writeChunkedBatch(async () => {
329
+ * for (const user of thousandsOfUsers) {
330
+ * await UserRepo.save({}, user);
331
+ * // Automatically commits every 500 operations
332
+ * }
333
+ * });
334
+ * ```
335
+ *
336
+ * @param fn - Function to run with automatic batch chunking
337
+ */
338
+ async function writeChunkedBatch(fn) {
339
+ const adapter = getFirestoreAdapter();
340
+ const state = {
341
+ batch: adapter.batch(),
342
+ operationCount: 0
343
+ };
344
+ const flushIfNeeded = async () => {
345
+ if (state.operationCount >= MAX_BATCH_SIZE) {
346
+ await state.batch.commit();
347
+ state.batch = adapter.batch();
348
+ state.operationCount = 0;
349
+ }
350
+ };
351
+ const trackedBatch = {
352
+ set(docRef, data) {
353
+ state.batch.set(docRef, data);
354
+ state.operationCount++;
355
+ flushIfNeeded();
356
+ return trackedBatch;
357
+ },
358
+ update(docRef, data) {
359
+ state.batch.update(docRef, data);
360
+ state.operationCount++;
361
+ flushIfNeeded();
362
+ return trackedBatch;
363
+ },
364
+ delete(docRef) {
365
+ state.batch.delete(docRef);
366
+ state.operationCount++;
367
+ flushIfNeeded();
368
+ return trackedBatch;
369
+ },
370
+ async commit() {}
371
+ };
372
+ await runWithBatch(trackedBatch, fn);
373
+ if (state.operationCount > 0) await state.batch.commit();
374
+ }
375
+
376
+ //#endregion
377
+ //#region src/model.hooks.ts
378
+ /**
379
+ * Merge multiple repository hooks into a single hooks object.
380
+ * When multiple hooks define the same method, they are chained in order.
381
+ */
382
+ function mergeRepositoryHooks(...hooksList) {
383
+ const filtered = hooksList.filter((h) => h !== void 0);
384
+ if (filtered.length === 0) return {};
385
+ if (filtered.length === 1) return filtered[0] ?? {};
386
+ const result = {};
387
+ const beforeSave = chainDataHooks(filtered.map((h) => h.beforeSave));
388
+ if (beforeSave) result.beforeSave = beforeSave;
389
+ const afterSave = chainVoidHooks(filtered.map((h) => h.afterSave));
390
+ if (afterSave) result.afterSave = afterSave;
391
+ const beforeUpdate = chainDataHooks(filtered.map((h) => h.beforeUpdate));
392
+ if (beforeUpdate) result.beforeUpdate = beforeUpdate;
393
+ const afterUpdate = chainVoidHooks(filtered.map((h) => h.afterUpdate));
394
+ if (afterUpdate) result.afterUpdate = afterUpdate;
395
+ const beforeDelete = chainVoidHooksNoData(filtered.map((h) => h.beforeDelete));
396
+ if (beforeDelete) result.beforeDelete = beforeDelete;
397
+ const afterDelete = chainVoidHooksNoData(filtered.map((h) => h.afterDelete));
398
+ if (afterDelete) result.afterDelete = afterDelete;
399
+ const afterGet = chainNullableDataHooks(filtered.map((h) => h.afterGet));
400
+ if (afterGet) result.afterGet = afterGet;
401
+ const afterFind = chainArrayDataHooks(filtered.map((h) => h.afterFind));
402
+ if (afterFind) result.afterFind = afterFind;
403
+ return result;
404
+ }
405
+ /**
406
+ * Merge multiple model hooks into a single hooks object.
407
+ */
408
+ function mergeModelHooks(...hooksList) {
409
+ const filtered = hooksList.filter((h) => h !== void 0);
410
+ if (filtered.length === 0) return {};
411
+ if (filtered.length === 1) return filtered[0] ?? {};
412
+ const result = {};
413
+ const onConstruct = chainModelDataHooks(filtered.map((h) => h.onConstruct));
414
+ if (onConstruct) result.onConstruct = onConstruct;
415
+ const onValidate = chainModelVoidHooks(filtered.map((h) => h.onValidate));
416
+ if (onValidate) result.onValidate = onValidate;
417
+ return result;
418
+ }
419
+ /**
420
+ * Merge feature hooks (combines both model and repository hooks).
421
+ */
422
+ function mergeFeatureHooks(...hooksList) {
423
+ const filtered = hooksList.filter((h) => h !== void 0);
424
+ if (filtered.length === 0) return {};
425
+ return {
426
+ model: mergeModelHooks(...filtered.map((h) => h.model)),
427
+ repository: mergeRepositoryHooks(...filtered.map((h) => h.repository))
428
+ };
429
+ }
430
+ function chainDataHooks(hooks) {
431
+ const filtered = hooks.filter((h) => h !== void 0);
432
+ if (filtered.length === 0) return void 0;
433
+ if (filtered.length === 1) return filtered[0];
434
+ return (data, context) => {
435
+ let result = data;
436
+ for (const hook of filtered) result = hook(result, context);
437
+ return result;
438
+ };
439
+ }
440
+ function chainVoidHooks(hooks) {
441
+ const filtered = hooks.filter((h) => h !== void 0);
442
+ if (filtered.length === 0) return void 0;
443
+ if (filtered.length === 1) return filtered[0];
444
+ return (data, context) => {
445
+ for (const hook of filtered) hook(data, context);
446
+ };
447
+ }
448
+ function chainVoidHooksNoData(hooks) {
449
+ const filtered = hooks.filter((h) => h !== void 0);
450
+ if (filtered.length === 0) return void 0;
451
+ if (filtered.length === 1) return filtered[0];
452
+ return (context) => {
453
+ for (const hook of filtered) hook(context);
454
+ };
455
+ }
456
+ function chainNullableDataHooks(hooks) {
457
+ const filtered = hooks.filter((h) => h !== void 0);
458
+ if (filtered.length === 0) return void 0;
459
+ if (filtered.length === 1) return filtered[0];
460
+ return (data, context) => {
461
+ let result = data;
462
+ for (const hook of filtered) result = hook(result, context);
463
+ return result;
464
+ };
465
+ }
466
+ function chainArrayDataHooks(hooks) {
467
+ const filtered = hooks.filter((h) => h !== void 0);
468
+ if (filtered.length === 0) return void 0;
469
+ if (filtered.length === 1) return filtered[0];
470
+ return (data, context) => {
471
+ let result = data;
472
+ for (const hook of filtered) result = hook(result, context);
473
+ return result;
474
+ };
475
+ }
476
+ function chainModelDataHooks(hooks) {
477
+ const filtered = hooks.filter((h) => h !== void 0);
478
+ if (filtered.length === 0) return void 0;
479
+ if (filtered.length === 1) return filtered[0];
480
+ return (props, context) => {
481
+ let result = props;
482
+ for (const hook of filtered) result = hook(result, context);
483
+ return result;
484
+ };
485
+ }
486
+ function chainModelVoidHooks(hooks) {
487
+ const filtered = hooks.filter((h) => h !== void 0);
488
+ if (filtered.length === 0) return void 0;
489
+ if (filtered.length === 1) return filtered[0];
490
+ return (data, context) => {
491
+ for (const hook of filtered) hook(data, context);
492
+ };
493
+ }
494
+
495
+ //#endregion
496
+ //#region src/model.metadata.ts
497
+ /**
498
+ * Symbol used to store metadata on model classes.
499
+ * Using a symbol prevents conflicts with user-defined properties.
500
+ */
501
+ const MODEL_METADATA = Symbol.for("fyrestack.model.metadata");
502
+ /**
503
+ * Get metadata from a model class.
504
+ */
505
+ function getModelMetadata(ModelClass) {
506
+ if (ModelClass !== null && typeof ModelClass === "function" && MODEL_METADATA in ModelClass) return ModelClass[MODEL_METADATA];
507
+ }
508
+ /**
509
+ * Get repository hooks from a model class.
510
+ */
511
+ function getRepositoryHooks(ModelClass) {
512
+ return getModelMetadata(ModelClass)?.hooks?.repository;
513
+ }
514
+ /**
515
+ * Get model hooks from a model class.
516
+ */
517
+ function getModelHooks(ModelClass) {
518
+ return getModelMetadata(ModelClass)?.hooks?.model;
519
+ }
520
+ /**
521
+ * Check if a model has any repository hooks registered.
522
+ */
523
+ function hasRepositoryHooks(ModelClass) {
524
+ const hooks = getRepositoryHooks(ModelClass);
525
+ return hooks !== void 0 && Object.keys(hooks).length > 0;
526
+ }
527
+ /**
528
+ * Check if a model has a specific feature registered.
529
+ */
530
+ function hasFeature(ModelClass, featureName) {
531
+ return getModelMetadata(ModelClass)?.features?.[featureName] !== void 0;
532
+ }
533
+ /**
534
+ * Get configuration for a specific feature.
535
+ */
536
+ function getFeatureConfig(ModelClass, featureName) {
537
+ return getModelMetadata(ModelClass)?.features?.[featureName];
538
+ }
539
+ /**
540
+ * Create metadata for a feature.
541
+ * Used by feature functions like withTimestamps() to build metadata.
542
+ *
543
+ * @param featureName - Unique name for the feature
544
+ * @param config - Feature-specific configuration (for introspection)
545
+ * @param hooks - Hooks to register
546
+ * @param existingMetadata - Existing metadata to merge with
547
+ */
548
+ function createFeatureMetadata(featureName, config, hooks, existingMetadata) {
549
+ return {
550
+ hooks: mergeFeatureHooks(existingMetadata?.hooks, hooks),
551
+ features: {
552
+ ...existingMetadata?.features,
553
+ [featureName]: config
554
+ }
555
+ };
556
+ }
557
+ /**
558
+ * Default timestamp configuration.
559
+ */
560
+ const DEFAULT_TIMESTAMPS = {
561
+ createdAt: "createdAt",
562
+ updatedAt: "updatedAt",
563
+ useServerTimestamp: true
564
+ };
565
+ /**
566
+ * Default audit configuration.
567
+ */
568
+ const DEFAULT_AUDIT = {
569
+ createdBy: "createdBy",
570
+ updatedBy: "updatedBy"
571
+ };
572
+ /**
573
+ * Default status configuration.
574
+ */
575
+ const DEFAULT_STATUS = {
576
+ field: "status",
577
+ values: [
578
+ "active",
579
+ "inactive",
580
+ "deleted"
581
+ ],
582
+ defaultValue: "active",
583
+ deletedValue: "deleted",
584
+ deletedAt: "deletedAt"
585
+ };
586
+ /**
587
+ * Check if a model has timestamps feature.
588
+ */
589
+ function hasTimestamps(ModelClass) {
590
+ return hasFeature(ModelClass, "timestamps");
591
+ }
592
+ /**
593
+ * Get timestamps configuration from a model class.
594
+ */
595
+ function getTimestampsConfig(ModelClass) {
596
+ return getFeatureConfig(ModelClass, "timestamps");
597
+ }
598
+ /**
599
+ * Check if a model has audit feature.
600
+ */
601
+ function hasAudit(ModelClass) {
602
+ return hasFeature(ModelClass, "audit");
603
+ }
604
+ /**
605
+ * Get audit configuration from a model class.
606
+ */
607
+ function getAuditConfig(ModelClass) {
608
+ return getFeatureConfig(ModelClass, "audit");
609
+ }
610
+ /**
611
+ * Check if a model has status feature.
612
+ */
613
+ function hasStatus(ModelClass) {
614
+ return hasFeature(ModelClass, "status");
615
+ }
616
+ /**
617
+ * Get status configuration from a model class.
618
+ */
619
+ function getStatusConfig(ModelClass) {
620
+ return getFeatureConfig(ModelClass, "status");
621
+ }
622
+
623
+ //#endregion
624
+ //#region src/model.types.ts
625
+ /**
626
+ * Symbol for storing the schema on model instances (internal use).
627
+ */
628
+ const MODEL_SCHEMA = Symbol.for("fyrestack.model.schema");
629
+
630
+ //#endregion
631
+ //#region src/model.ts
632
+ function model(...features) {
633
+ const innerStore = features.reduce((store, feature) => feature(store), getInitialInnerStore());
634
+ const modelSchema = innerStore.schema;
635
+ const modelMethods = innerStore.methods;
636
+ class Model {
637
+ constructor(data) {
638
+ this[MODEL_SCHEMA] = modelSchema;
639
+ let validatedProps = {};
640
+ if (data !== void 0 && data !== null) if ("~standard" in modelSchema) {
641
+ const result = modelSchema["~standard"].validate(data);
642
+ if ("issues" in result && result.issues) throw toValidationError(result.issues);
643
+ if ("value" in result) validatedProps = result.value;
644
+ } else validatedProps = data;
645
+ for (const [key, value] of Object.entries(validatedProps)) this[key] = value;
646
+ for (const [key, method] of Object.entries(modelMethods)) this[key] = method;
647
+ }
648
+ /**
649
+ * Validate the current model data against the schema.
650
+ * @returns ValidationResult with validated value or error
651
+ */
652
+ validate() {
653
+ const schema = this[MODEL_SCHEMA];
654
+ const data = this.toObject();
655
+ if (!("~standard" in schema)) return {
656
+ valid: true,
657
+ value: data
658
+ };
659
+ const result = schema["~standard"].validate(data);
660
+ if ("issues" in result && result.issues) return {
661
+ valid: false,
662
+ error: toValidationError(result.issues)
663
+ };
664
+ if ("value" in result) return {
665
+ valid: true,
666
+ value: result.value
667
+ };
668
+ return {
669
+ valid: true,
670
+ value: data
671
+ };
672
+ }
673
+ /**
674
+ * Check if the current model data is valid.
675
+ * @returns true if valid, false otherwise
676
+ */
677
+ isValid() {
678
+ return this.validate().valid;
679
+ }
680
+ /**
681
+ * Convert model instance to a plain object (excludes methods).
682
+ * Used for Firestore writes and serialization.
683
+ */
684
+ toObject() {
685
+ const result = {};
686
+ for (const key of Object.keys(this)) {
687
+ const value = this[key];
688
+ if (typeof value !== "function") result[key] = value;
689
+ }
690
+ return result;
691
+ }
692
+ }
693
+ const metadata = innerStore._metadata;
694
+ if (metadata) Model[MODEL_METADATA] = metadata;
695
+ return Model;
696
+ }
697
+ function getInitialInnerStore() {
698
+ return {
699
+ schema: {},
700
+ props: {},
701
+ methods: {}
702
+ };
703
+ }
704
+ /**
705
+ * Convert Standard Schema issues to ValidationIssues.
706
+ * Handles various path formats from different schema libraries.
707
+ */
708
+ function toValidationError(issues) {
709
+ const mappedIssues = issues.map((issue) => {
710
+ let path = [];
711
+ if (Array.isArray(issue.path)) path = issue.path.map((p) => {
712
+ if (typeof p === "string" || typeof p === "number" || typeof p === "symbol") return String(p);
713
+ if (p !== null && typeof p === "object" && "key" in p) return String(p.key);
714
+ return String(p);
715
+ });
716
+ return {
717
+ path,
718
+ message: issue.message
719
+ };
720
+ });
721
+ return new ValidationError(mappedIssues.length > 0 && mappedIssues[0] !== void 0 ? mappedIssues.length === 1 ? mappedIssues[0].message : `Validation failed with ${String(mappedIssues.length)} issues` : "Validation failed", mappedIssues);
722
+ }
723
+
724
+ //#endregion
725
+ //#region src/model.with-schema.ts
726
+ function withSchema(schema) {
727
+ return (store) => ({
728
+ schema,
729
+ props: {},
730
+ methods: store.methods
731
+ });
732
+ }
733
+
734
+ //#endregion
735
+ //#region src/model.with-timestamps.ts
736
+ /**
737
+ * Create repository hooks for timestamp management.
738
+ * This is the core implementation extracted into hooks.
739
+ */
740
+ function createTimestampsHooks(config) {
741
+ return {
742
+ beforeSave(data, context) {
743
+ const timestamp = config.useServerTimestamp ? context.adapter.serverTimestamp() : /* @__PURE__ */ new Date();
744
+ if (config.createdAt !== false && (data[config.createdAt] === void 0 || data[config.createdAt] === null)) data[config.createdAt] = timestamp;
745
+ if (config.updatedAt !== false) data[config.updatedAt] = timestamp;
746
+ return data;
747
+ },
748
+ beforeUpdate(updates, context) {
749
+ if (config.updatedAt !== false) {
750
+ const timestamp = config.useServerTimestamp ? context.adapter.serverTimestamp() : /* @__PURE__ */ new Date();
751
+ updates[config.updatedAt] = timestamp;
752
+ }
753
+ return updates;
754
+ }
755
+ };
756
+ }
757
+ /**
758
+ * Add automatic timestamp management to a model.
759
+ *
760
+ * This feature registers hooks that:
761
+ * - Set `createdAt` on save (if not already set)
762
+ * - Set `updatedAt` on save and update
763
+ *
764
+ * @example
765
+ * ```typescript
766
+ * // Basic usage with defaults (createdAt, updatedAt)
767
+ * const UserModel = model(
768
+ * withSchema(UserSchema),
769
+ * withTimestamps(),
770
+ * );
771
+ *
772
+ * // Custom field names
773
+ * const PostModel = model(
774
+ * withSchema(PostSchema),
775
+ * withTimestamps({ createdAt: 'publishedAt', updatedAt: 'modifiedAt' }),
776
+ * );
777
+ *
778
+ * // Disable one field
779
+ * const LogModel = model(
780
+ * withSchema(LogSchema),
781
+ * withTimestamps({ updatedAt: false }), // Only track creation
782
+ * );
783
+ *
784
+ * // Use client-side timestamps (for offline support)
785
+ * const OfflineModel = model(
786
+ * withSchema(OfflineSchema),
787
+ * withTimestamps({ useServerTimestamp: false }),
788
+ * );
789
+ * ```
790
+ *
791
+ * @param options - Optional configuration for field names and behavior
792
+ */
793
+ function withTimestamps(options) {
794
+ const config = {
795
+ createdAt: options?.createdAt ?? DEFAULT_TIMESTAMPS.createdAt,
796
+ updatedAt: options?.updatedAt ?? DEFAULT_TIMESTAMPS.updatedAt,
797
+ useServerTimestamp: options?.useServerTimestamp ?? DEFAULT_TIMESTAMPS.useServerTimestamp
798
+ };
799
+ const hooks = createTimestampsHooks(config);
800
+ return (store) => {
801
+ const newMetadata = createFeatureMetadata("timestamps", config, { repository: hooks }, store._metadata);
802
+ return {
803
+ schema: store.schema,
804
+ props: store.props,
805
+ methods: store.methods,
806
+ _metadata: newMetadata
807
+ };
808
+ };
809
+ }
810
+
811
+ //#endregion
812
+ //#region src/repository.helpers.ts
813
+ /**
814
+ * Composable Repository Implementation for Firestore
815
+ */
816
+ function createTypedQueryBuilder(queryRef) {
817
+ return {
818
+ where(field, op, value) {
819
+ return createTypedQueryBuilder(queryRef.where(field, op, value));
820
+ },
821
+ orderBy(field, direction) {
822
+ return createTypedQueryBuilder(queryRef.orderBy(field, direction));
823
+ },
824
+ limit(count) {
825
+ return createTypedQueryBuilder(queryRef.limit(count));
826
+ },
827
+ startAt(...values) {
828
+ return createTypedQueryBuilder(queryRef.startAt(...values));
829
+ },
830
+ startAfter(...values) {
831
+ return createTypedQueryBuilder(queryRef.startAfter(...values));
832
+ },
833
+ endAt(...values) {
834
+ return createTypedQueryBuilder(queryRef.endAt(...values));
835
+ },
836
+ endBefore(...values) {
837
+ return createTypedQueryBuilder(queryRef.endBefore(...values));
838
+ },
839
+ _queryRef: queryRef
840
+ };
841
+ }
842
+ function getQueryRef(builder) {
843
+ return builder._queryRef;
844
+ }
845
+ /**
846
+ * Regex for valid path parameter values.
847
+ * Allows alphanumeric characters, underscores, and hyphens.
848
+ */
849
+ const VALID_PATH_PARAM_REGEX = /^[a-zA-Z0-9_-]+$/;
850
+ /**
851
+ * Validate a single path parameter value.
852
+ * @throws PathParamError if the value is invalid
853
+ */
854
+ function validatePathParam(paramName, value) {
855
+ if (!value || value.length === 0) throw new PathParamError(paramName, value, "cannot be empty");
856
+ if (value.includes("/")) throw new PathParamError(paramName, value, "cannot contain forward slashes");
857
+ if (value.includes("..")) throw new PathParamError(paramName, value, "cannot contain path traversal (..)");
858
+ if (value.includes("\0")) throw new PathParamError(paramName, value, "cannot contain null bytes");
859
+ if (!VALID_PATH_PARAM_REGEX.test(value)) throw new PathParamError(paramName, value, "must only contain alphanumeric characters, underscores, or hyphens");
860
+ }
861
+ /**
862
+ * Build a collection path from template and params.
863
+ * Validates all path parameters before building.
864
+ * @throws PathParamError if any parameter is invalid
865
+ */
866
+ function buildCollectionPath(pathTemplate, params) {
867
+ let result = pathTemplate;
868
+ for (const [key, value] of Object.entries(params)) {
869
+ validatePathParam(key, value);
870
+ result = result.replace(`{${key}}`, value);
871
+ }
872
+ return result;
873
+ }
874
+ /**
875
+ * Extract model ID from model instance or string.
876
+ * @throws ModelError if the model doesn't have a valid ID
877
+ */
878
+ function extractId(modelOrId) {
879
+ if (typeof modelOrId === "string") {
880
+ if (!modelOrId) throw new ModelError("Document ID cannot be empty");
881
+ return modelOrId;
882
+ }
883
+ if (modelOrId !== null && typeof modelOrId === "object" && "id" in modelOrId && typeof modelOrId.id === "string") {
884
+ if (!modelOrId.id) throw new ModelError("Model ID cannot be empty");
885
+ return modelOrId.id;
886
+ }
887
+ throw new ModelError("Model must have a string \"id\" property");
888
+ }
889
+ /**
890
+ * Type guard to check if a model has a validate() method.
891
+ */
892
+ function isValidatable(model$1) {
893
+ return "validate" in model$1 && typeof model$1.validate === "function";
894
+ }
895
+ /**
896
+ * Type guard to check if a model has a toObject() method.
897
+ */
898
+ function hasToObject(model$1) {
899
+ return "toObject" in model$1 && typeof model$1.toObject === "function";
900
+ }
901
+ /**
902
+ * Validate a model instance before write operations.
903
+ * @throws ValidationError if validation fails
904
+ */
905
+ function validateModel(model$1) {
906
+ if (model$1 === null || typeof model$1 !== "object") throw new ValidationError("Model must be an object");
907
+ if (isValidatable(model$1)) {
908
+ const result = model$1.validate();
909
+ if (!result.valid && result.error) throw result.error;
910
+ }
911
+ }
912
+ /**
913
+ * Convert model to plain object for Firestore.
914
+ * Uses toObject() if available, otherwise spreads the object.
915
+ */
916
+ function modelToObject(model$1) {
917
+ if (model$1 !== null && typeof model$1 === "object") {
918
+ if (hasToObject(model$1)) return model$1.toObject();
919
+ const result = {};
920
+ for (const [key, value] of Object.entries(model$1)) if (typeof value !== "function") result[key] = value;
921
+ return result;
922
+ }
923
+ throw new ModelError("Model must be an object");
924
+ }
925
+
926
+ //#endregion
927
+ //#region src/repository.ts
928
+ function repository(config, ...features) {
929
+ const { path, model: ModelClass } = config;
930
+ const hooks = getRepositoryHooks(ModelClass);
931
+ class Repository {
932
+ getFirestore() {
933
+ return getFirestoreAdapter();
934
+ }
935
+ getCollectionRef(params) {
936
+ const collectionPath = buildCollectionPath(path, params);
937
+ return this.getFirestore().collection(collectionPath);
938
+ }
939
+ getDocumentRef(params, id) {
940
+ return this.getCollectionRef(params).doc(id);
941
+ }
942
+ /**
943
+ * Create hook context for repository hooks.
944
+ */
945
+ createHookContext(params, id) {
946
+ const context = {
947
+ adapter: this.getFirestore(),
948
+ modelClass: ModelClass,
949
+ path,
950
+ params
951
+ };
952
+ if (id !== void 0) context.id = id;
953
+ return context;
954
+ }
955
+ async get(params, id) {
956
+ try {
957
+ const docRef = this.getDocumentRef(params, id);
958
+ const ctx = getCurrentContext();
959
+ const tx = isTransactionContext(ctx) ? ctx.tx : void 0;
960
+ const snapshot = tx ? await tx.get(docRef) : await docRef.get();
961
+ if (!snapshot.exists) return null;
962
+ const data = snapshot.data();
963
+ if (!data) return null;
964
+ let result = new ModelClass({
965
+ ...data,
966
+ id: snapshot.id
967
+ });
968
+ if (hooks?.afterGet) {
969
+ const context = this.createHookContext(params, id);
970
+ result = hooks.afterGet(result, context);
971
+ }
972
+ return result;
973
+ } catch (error) {
974
+ if (error instanceof PathParamError || error instanceof ValidationError) throw error;
975
+ throw FirestoreError.wrap("get", error);
976
+ }
977
+ }
978
+ async find(params, queryFn) {
979
+ try {
980
+ const collectionRef = this.getCollectionRef(params);
981
+ let queryRef = collectionRef;
982
+ if (queryFn) queryRef = getQueryRef(queryFn(createTypedQueryBuilder(collectionRef)));
983
+ let results = (await queryRef.get()).docs.map((doc) => {
984
+ return new ModelClass({
985
+ ...doc.data(),
986
+ id: doc.id
987
+ });
988
+ });
989
+ if (hooks?.afterFind) {
990
+ const context = this.createHookContext(params);
991
+ results = hooks.afterFind(results, context);
992
+ }
993
+ return results;
994
+ } catch (error) {
995
+ if (error instanceof PathParamError || error instanceof ValidationError) throw error;
996
+ throw FirestoreError.wrap("find", error);
997
+ }
998
+ }
999
+ async save(params, model$1) {
1000
+ try {
1001
+ validateModel(model$1);
1002
+ const id = extractId(model$1);
1003
+ const docRef = this.getDocumentRef(params, id);
1004
+ let data = modelToObject(model$1);
1005
+ if (hooks?.beforeSave) {
1006
+ const context = this.createHookContext(params, id);
1007
+ data = hooks.beforeSave(data, context);
1008
+ }
1009
+ const ctx = getCurrentContext();
1010
+ if (isTransactionContext(ctx)) ctx.tx.set(docRef, data);
1011
+ else if (isBatchContext(ctx)) ctx.batch.set(docRef, data);
1012
+ else await docRef.set(data);
1013
+ if (hooks?.afterSave) {
1014
+ const context = this.createHookContext(params, id);
1015
+ hooks.afterSave(data, context);
1016
+ }
1017
+ } catch (error) {
1018
+ if (error instanceof PathParamError || error instanceof ValidationError || error instanceof ModelError) throw error;
1019
+ throw FirestoreError.wrap("save", error);
1020
+ }
1021
+ }
1022
+ async update(params, modelOrId, updates) {
1023
+ try {
1024
+ const id = extractId(modelOrId);
1025
+ const docRef = this.getDocumentRef(params, id);
1026
+ let updateData = { ...updates };
1027
+ if (hooks?.beforeUpdate) {
1028
+ const context = this.createHookContext(params, id);
1029
+ updateData = hooks.beforeUpdate(updateData, context);
1030
+ }
1031
+ const ctx = getCurrentContext();
1032
+ if (isTransactionContext(ctx)) ctx.tx.update(docRef, updateData);
1033
+ else if (isBatchContext(ctx)) ctx.batch.update(docRef, updateData);
1034
+ else await docRef.update(updateData);
1035
+ if (hooks?.afterUpdate) {
1036
+ const context = this.createHookContext(params, id);
1037
+ hooks.afterUpdate(updateData, context);
1038
+ }
1039
+ } catch (error) {
1040
+ if (error instanceof PathParamError || error instanceof ModelError) throw error;
1041
+ throw FirestoreError.wrap("update", error);
1042
+ }
1043
+ }
1044
+ async delete(params, modelOrId) {
1045
+ try {
1046
+ const id = extractId(modelOrId);
1047
+ const docRef = this.getDocumentRef(params, id);
1048
+ if (hooks?.beforeDelete) {
1049
+ const context = this.createHookContext(params, id);
1050
+ hooks.beforeDelete(context);
1051
+ }
1052
+ const ctx = getCurrentContext();
1053
+ if (isTransactionContext(ctx)) ctx.tx.delete(docRef);
1054
+ else if (isBatchContext(ctx)) ctx.batch.delete(docRef);
1055
+ else await docRef.delete();
1056
+ if (hooks?.afterDelete) {
1057
+ const context = this.createHookContext(params, id);
1058
+ hooks.afterDelete(context);
1059
+ }
1060
+ } catch (error) {
1061
+ if (error instanceof PathParamError || error instanceof ModelError) throw error;
1062
+ throw FirestoreError.wrap("delete", error);
1063
+ }
1064
+ }
1065
+ }
1066
+ if (features.length > 0) {
1067
+ const repoInstance = new Repository();
1068
+ const innerRepo = {
1069
+ model: ModelClass,
1070
+ path,
1071
+ methods: {
1072
+ get: repoInstance.get.bind(repoInstance),
1073
+ find: repoInstance.find.bind(repoInstance),
1074
+ save: repoInstance.save.bind(repoInstance),
1075
+ update: repoInstance.update.bind(repoInstance),
1076
+ delete: repoInstance.delete.bind(repoInstance)
1077
+ },
1078
+ getCollection: repoInstance["getCollectionRef"].bind(repoInstance),
1079
+ getDocument: repoInstance["getDocumentRef"].bind(repoInstance)
1080
+ };
1081
+ let currentMethods = innerRepo.methods;
1082
+ for (const feature of features) currentMethods = feature({
1083
+ ...innerRepo,
1084
+ methods: currentMethods
1085
+ });
1086
+ const FinalRepository = class extends Repository {
1087
+ constructor() {
1088
+ super();
1089
+ for (const [key, method] of Object.entries(currentMethods)) if (!(key in this)) this[key] = method;
1090
+ }
1091
+ };
1092
+ return FinalRepository;
1093
+ }
1094
+ return Repository;
1095
+ }
1096
+
1097
+ //#endregion
1098
+ //#region src/repository.with-methods.ts
1099
+ /**
1100
+ * Add custom methods to a repository
1101
+ */
1102
+ function withMethods(methodsFactory) {
1103
+ return (repo) => {
1104
+ const newMethods = methodsFactory(repo);
1105
+ return {
1106
+ ...repo.methods,
1107
+ ...newMethods
1108
+ };
1109
+ };
1110
+ }
1111
+
1112
+ //#endregion
1113
+ //#region src/transaction.ts
1114
+ /**
1115
+ * Transaction Support for FyreStack Database
1116
+ *
1117
+ * Provides atomic read-write operations across multiple repositories.
1118
+ * Context is automatically propagated using AsyncContext.
1119
+ */
1120
+ /**
1121
+ * Run operations within a Firestore transaction.
1122
+ *
1123
+ * Transactions provide:
1124
+ * - Atomic reads and writes across multiple repositories
1125
+ * - Automatic retry on contention (up to maxAttempts)
1126
+ * - Consistent view of data (all reads see same snapshot)
1127
+ * - All-or-nothing semantics (all writes succeed or all fail)
1128
+ *
1129
+ * The transaction context is **automatically propagated** to all repository
1130
+ * methods called within the transaction function - no need to pass context explicitly.
1131
+ *
1132
+ * @example
1133
+ * ```typescript
1134
+ * // Transfer funds between accounts atomically
1135
+ * const result = await runTransaction(async () => {
1136
+ * const fromAccount = await AccountRepo.get({}, fromId);
1137
+ * const toAccount = await AccountRepo.get({}, toId);
1138
+ *
1139
+ * if (!fromAccount || !toAccount) throw new Error('Account not found');
1140
+ * if (fromAccount.balance < amount) throw new Error('Insufficient funds');
1141
+ *
1142
+ * await AccountRepo.update({}, fromId, {
1143
+ * balance: fromAccount.balance - amount
1144
+ * });
1145
+ *
1146
+ * await AccountRepo.update({}, toId, {
1147
+ * balance: toAccount.balance + amount
1148
+ * });
1149
+ *
1150
+ * return { newBalance: fromAccount.balance - amount };
1151
+ * });
1152
+ * ```
1153
+ *
1154
+ * @example
1155
+ * ```typescript
1156
+ * // Cross-repository transaction
1157
+ * await runTransaction(async () => {
1158
+ * const order = await OrderRepo.get({}, orderId);
1159
+ * const user = await UserRepo.get({}, order.userId);
1160
+ *
1161
+ * await OrderRepo.update({}, orderId, { status: 'paid' });
1162
+ * await UserRepo.update({}, user.id, {
1163
+ * balance: user.balance - order.total
1164
+ * });
1165
+ * });
1166
+ * ```
1167
+ *
1168
+ * @param fn - Async function to run within the transaction
1169
+ * @param options - Transaction options (maxAttempts, etc.)
1170
+ * @returns Result of the transaction function
1171
+ */
1172
+ async function runTransaction(fn, options) {
1173
+ return getFirestoreAdapter().runTransaction(async (tx) => {
1174
+ return runWithTransaction(tx, fn);
1175
+ }, options);
1176
+ }
1177
+
1178
+ //#endregion
1179
+ exports.AdapterNotInitializedError = AdapterNotInitializedError;
1180
+ exports.DEFAULT_AUDIT = DEFAULT_AUDIT;
1181
+ exports.DEFAULT_STATUS = DEFAULT_STATUS;
1182
+ exports.DEFAULT_TIMESTAMPS = DEFAULT_TIMESTAMPS;
1183
+ exports.FirestoreError = FirestoreError;
1184
+ exports.FyreStackError = FyreStackError;
1185
+ exports.MAX_BATCH_SIZE = MAX_BATCH_SIZE;
1186
+ exports.MODEL_METADATA = MODEL_METADATA;
1187
+ exports.MODEL_SCHEMA = MODEL_SCHEMA;
1188
+ exports.ModelError = ModelError;
1189
+ exports.NotFoundError = NotFoundError;
1190
+ exports.PathParamError = PathParamError;
1191
+ exports.ValidationError = ValidationError;
1192
+ exports._resetFirestoreAdapter = _resetFirestoreAdapter;
1193
+ exports.createFeatureMetadata = createFeatureMetadata;
1194
+ exports.getAuditConfig = getAuditConfig;
1195
+ exports.getCurrentBatch = getCurrentBatch;
1196
+ exports.getCurrentContext = getCurrentContext;
1197
+ exports.getCurrentTransaction = getCurrentTransaction;
1198
+ exports.getFeatureConfig = getFeatureConfig;
1199
+ exports.getFirestoreAdapter = getFirestoreAdapter;
1200
+ exports.getModelHooks = getModelHooks;
1201
+ exports.getModelMetadata = getModelMetadata;
1202
+ exports.getRepositoryHooks = getRepositoryHooks;
1203
+ exports.getStatusConfig = getStatusConfig;
1204
+ exports.getTimestampsConfig = getTimestampsConfig;
1205
+ exports.hasAudit = hasAudit;
1206
+ exports.hasFeature = hasFeature;
1207
+ exports.hasRepositoryHooks = hasRepositoryHooks;
1208
+ exports.hasStatus = hasStatus;
1209
+ exports.hasTimestamps = hasTimestamps;
1210
+ exports.isBatchContext = isBatchContext;
1211
+ exports.isInBatch = isInBatch;
1212
+ exports.isInTransaction = isInTransaction;
1213
+ exports.isServerTimestamp = isServerTimestamp;
1214
+ exports.isTransactionContext = isTransactionContext;
1215
+ exports.isValidResult = isValidResult;
1216
+ exports.mergeFeatureHooks = mergeFeatureHooks;
1217
+ exports.mergeModelHooks = mergeModelHooks;
1218
+ exports.mergeRepositoryHooks = mergeRepositoryHooks;
1219
+ exports.model = model;
1220
+ exports.repository = repository;
1221
+ exports.runTransaction = runTransaction;
1222
+ exports.runWithBatch = runWithBatch;
1223
+ exports.runWithTransaction = runWithTransaction;
1224
+ exports.setFirestoreAdapter = setFirestoreAdapter;
1225
+ exports.withMethods = withMethods;
1226
+ exports.withSchema = withSchema;
1227
+ exports.withTimestamps = withTimestamps;
1228
+ exports.writeBatch = writeBatch;
1229
+ exports.writeChunkedBatch = writeChunkedBatch;