@dereekb/firebase 13.36.0 → 13.38.0

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.
@@ -105,10 +105,15 @@ export declare abstract class AbstractFirestoreDocument<T, D extends AbstractFir
105
105
  */
106
106
  snapshot(): Promise<DocumentSnapshot<T>>;
107
107
  /**
108
- * Retrieves the data of the document, checking the cache first.
108
+ * Retrieves the data of the document, always fetching from Firestore.
109
109
  *
110
- * If a fresh cache entry exists, returns it without hitting Firestore.
111
- * Otherwise falls through to {@link snapshot} which populates the cache.
110
+ * Delegates to {@link snapshot}, so the read is unconditional and the cache is only WRITTEN to, not
111
+ * consulted — a caller that wants the cached value should read {@link cache} directly.
112
+ *
113
+ * The returned data is converter-applied: declared defaults are filled in, undeclared fields are
114
+ * stripped, and encoded fields (`firestoreEncodedArray`, `firestoreBitwiseSet`) are decoded. This is
115
+ * byte-for-byte what the model API's `readDocument` returns, which is what lets `dbx-cli` read the
116
+ * same document over either transport and get the same answer.
112
117
  *
113
118
  * @param options - Overrides forwarded to `DocumentSnapshot.data()`, if any.
114
119
  * @returns Resolves with the document data, or undefined when the document does not exist.
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import { type WebsiteLink, type GrantedRole, type WebsiteFileLink } from '@dereekb/model';
32
32
  import { type DateCellRange, type DateCellSchedule } from '@dereekb/date';
33
- import { type ModelFieldMapFunctionsConfig, type GetterOrValue, type Maybe, type ModelFieldMapConvertFunction, type PrimativeKey, type ReadKeyFunction, type ModelFieldMapFunctionsWithDefaultsConfig, type FilterUniqueStringsTransformConfig, type MapFunction, type FilterKeyValueTuplesInput, type ModelKey, type ToModelMapFunctionsInput, type ModelMapFunctionsRef, type LatLngPrecision, type LatLngString, type TimezoneString, type PrimativeKeyStringDencoderFunction, type PrimativeKeyDencoderFunction, type UnitedStatesAddress, type ZoomLevel, type FilterUniqueFunction, type BitwiseEncodedSet, type BitwiseObjectDencoder, type SortCompareFunctionRef, type TransformNumberFunctionConfigInput, type TransformStringFunctionConfigInput, type DecisionFunction, type ISO8601DateString, type FilterFunction } from '@dereekb/util';
33
+ import { type ModelFieldMapFunctionsConfig, type GetterOrValue, type Maybe, type ModelFieldMapConvertFunction, type PrimativeKey, type ReadKeyFunction, type ModelFieldMapFunctionsWithDefaultsConfig, type FilterUniqueStringsTransformConfig, type MapFunction, type FilterKeyValueTuplesInput, type CopyValueDeepConfig, type ModelKey, type ToModelMapFunctionsInput, type ModelMapFunctionsRef, type LatLngPrecision, type LatLngString, type TimezoneString, type PrimativeKeyStringDencoderFunction, type PrimativeKeyDencoderFunction, type UnitedStatesAddress, type ZoomLevel, type FilterUniqueFunction, type BitwiseEncodedSet, type BitwiseObjectDencoder, type SortCompareFunctionRef, type TransformNumberFunctionConfigInput, type TransformStringFunctionConfigInput, type DecisionFunction, type ISO8601DateString, type FilterFunction } from '@dereekb/util';
34
34
  import { type FirestoreModelData } from './snapshot.type';
35
35
  /**
36
36
  * Base configuration for Firestore field mapping.
@@ -315,6 +315,69 @@ export declare function optionalFirestoreField<V, D>(config?: OptionalFirestoreF
315
315
  * @returns A field mapping configuration for optional values
316
316
  */
317
317
  export declare function optionalFirestoreField<T>(config?: OptionalFirestoreFieldConfigWithOneTypeTransform<T>): FirestoreModelFieldMapFunctionsConfig<Maybe<T>, Maybe<T>>;
318
+ /**
319
+ * Configuration for {@link optionalFirestorePassthroughJsonField}.
320
+ *
321
+ * Extends {@link CopyValueDeepConfig}, which is what configures the filtering applied on the way in: by
322
+ * default `filter` strips `undefined` values at every depth. `arrayValues`, `filterEmptyValues`, and
323
+ * `transform` are passed through to the copy as-is.
324
+ *
325
+ * @template T - Type for both the model field and Firestore field
326
+ */
327
+ export interface OptionalFirestorePassthroughJsonFieldConfig<T extends object> extends CopyValueDeepConfig, Pick<OptionalFirestoreFieldConfig<T, T>, 'defaultReadValue'> {
328
+ /**
329
+ * Whether to store `null` instead of an object that has no keys left after filtering. Defaults to `false`.
330
+ */
331
+ readonly dontStoreIfEmpty?: boolean;
332
+ }
333
+ /**
334
+ * Creates a field mapping configuration for an optional object field that is stored as-is, aside from
335
+ * the values the filter strips out on the way in.
336
+ *
337
+ * This is the field for json a converter should not model: a third-party api response, a request config
338
+ * whose parameters the vendor extends without warning, an sdk-shaped payload. A strict converter would
339
+ * silently drop whatever it does not name; this one keeps everything.
340
+ *
341
+ * The one thing it does not keep is a value Firestore rejects. Firestore refuses an explicit `undefined`
342
+ * outright — one absent optional field anywhere in the payload fails the whole write — and such a value
343
+ * is exactly what assembling json from `Maybe` inputs produces (a usage object built from whichever
344
+ * token counts a response happened to report, a config a caller spread a `Maybe` into). Solved per-writer
345
+ * it has to be remembered at every call site; solved here it cannot be forgotten.
346
+ *
347
+ * The filtering is RECURSIVE, since json of this kind is nested and its interior is just as capable of
348
+ * carrying an `undefined` as its top level. Non-plain values are retained by reference, so a `Date` (or
349
+ * a `Timestamp`, or a `DocumentReference`) survives the copy intact.
350
+ *
351
+ * Filtering happens on WRITE only. Reads are the plain passthrough — no copy, no traversal — since data
352
+ * that came out of Firestore cannot contain the values being filtered in the first place.
353
+ *
354
+ * A top-level `null` still clears the field: {@link optionalFirestoreField} short-circuits `x == null`
355
+ * ahead of the transform, so `update({ myField: null })` is untouched by this.
356
+ *
357
+ * @param config - Filtering and storage configuration. Defaults to stripping `undefined` values at every depth.
358
+ * @returns A field mapping configuration for optional passthrough json values.
359
+ *
360
+ * @dbxModelSnapshotField
361
+ * @dbxModelSnapshotFieldCategory object
362
+ * @dbxModelSnapshotFieldOptional true
363
+ * @dbxModelSnapshotFieldTags json, passthrough, object, raw, optional, undefined, filter, recursive, factory
364
+ * @dbxModelSnapshotFieldRelated optional-firestore-field, firestore-pass-through-field, firestore-sub-object
365
+ * @template T - Type for both the model field and Firestore field
366
+ *
367
+ * @example
368
+ * ```ts
369
+ * fields: {
370
+ * // { model: 'm', temperature: undefined, provider: { only: ['openai'], sort: undefined } }
371
+ * // stores as { model: 'm', provider: { only: ['openai'] } }
372
+ * config: optionalFirestorePassthroughJsonField<MyVendorConfig>(),
373
+ * // store null rather than an empty object when nothing survives the filtering
374
+ * usage: optionalFirestorePassthroughJsonField<MyVendorUsage>({ filterEmptyValues: true, dontStoreIfEmpty: true })
375
+ * }
376
+ * ```
377
+ *
378
+ * @__NO_SIDE_EFFECTS__
379
+ */
380
+ export declare function optionalFirestorePassthroughJsonField<T extends object>(config?: OptionalFirestorePassthroughJsonFieldConfig<T>): FirestoreModelFieldMapFunctionsConfig<Maybe<T>, Maybe<T>>;
318
381
  /**
319
382
  * Configuration for a Firestore field with default model value but without conversion functions.
320
383
  *
@@ -21,6 +21,12 @@ export type FirebaseModelServiceContext = FirebasePermissionContext & FirebasePe
21
21
  * @template R - the granted role type for this model
22
22
  */
23
23
  export interface FirebaseModelService<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends GrantedRole = GrantedRole> extends FirebaseModelPermissionService<C, T, D, R>, FirebaseModelLoader<C, T, D>, FirebaseModelCollectionLoader<any, T, D> {
24
+ /**
25
+ * Whether this model is SERVER-ONLY: unreachable by any client, on any read path.
26
+ *
27
+ * See {@link FirebaseModelServiceConfig.serverOnly}.
28
+ */
29
+ readonly serverOnly?: boolean;
24
30
  }
25
31
  /**
26
32
  * Lazy getter for a {@link FirebaseModelService}, typically used in the service factory map.
@@ -33,6 +39,22 @@ export type FirebaseModelServiceGetter<C extends FirebaseModelServiceContext, T,
33
39
  * from the collection loader.
34
40
  */
35
41
  export interface FirebaseModelServiceConfig<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends GrantedRole = GrantedRole> extends Omit<FirebasePermissionServiceInstanceDelegate<C, T, D, R>, 'loadModelForKey'>, FirebaseModelCollectionLoader<C, T, D> {
42
+ /**
43
+ * Marks this model SERVER-ONLY: no client may read it, on any path.
44
+ *
45
+ * `roleMapForModel` and `firestore.rules` are two independent authorization systems, and the
46
+ * model API runs under the Admin SDK — so it evaluates the former and bypasses the latter
47
+ * entirely. For a model whose rules file has no match block (or an `allow read: if false`), that
48
+ * divergence is a LEAK: the rules say server-only, and the model API hands the document to a
49
+ * client anyway.
50
+ *
51
+ * Setting this closes the gap by refusing the read ahead of `useModel`, so both read paths agree.
52
+ * It is a routing decision, not a permission computation — hence a flag rather than a role.
53
+ *
54
+ * Opt-in by design: absent means today's behaviour, which is the backward-compatible direction
55
+ * for a framework. Downstream apps are unaffected until they set it.
56
+ */
57
+ readonly serverOnly?: boolean;
36
58
  }
37
59
  /**
38
60
  * Creates a {@link FirebaseModelService} that wires together model loading and permission evaluation.
@@ -78,7 +100,13 @@ export type LimitedInContextFirebaseModelService<C extends FirebasePermissionErr
78
100
  * Calling `service(modelOrKey)` returns an {@link InModelContextFirebaseModelService} with role checking and assertions.
79
101
  * Also provides `forKey(key)` for key-based lookup.
80
102
  */
81
- export type InContextFirebaseModelService<C extends FirebasePermissionErrorContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends GrantedRole = GrantedRole> = InModelContextFirebaseModelServiceFactory<C, T, D, R> & LimitedInContextFirebaseModelService<C, T, D, R>;
103
+ export type InContextFirebaseModelService<C extends FirebasePermissionErrorContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends GrantedRole = GrantedRole> = InModelContextFirebaseModelServiceFactory<C, T, D, R> & LimitedInContextFirebaseModelService<C, T, D, R> & {
104
+ /**
105
+ * Mirrors {@link FirebaseModelServiceConfig.serverOnly}, so a caller holding only the
106
+ * context-bound service can gate on it without reaching back for the config.
107
+ */
108
+ readonly serverOnly?: boolean;
109
+ };
82
110
  /**
83
111
  * Factory that binds a {@link FirebaseModelService} to a specific context, producing an {@link InContextFirebaseModelService}.
84
112
  */
@@ -593,6 +593,7 @@ export interface NotificationSendCheckpoints {
593
593
  *
594
594
  * @dbxModel
595
595
  * @dbxModelRead system
596
+ * @dbxModelServerOnly
596
597
  */
597
598
  export interface Notification extends NotificationSendFlags, NotificationSendCheckpoints {
598
599
  /**
@@ -753,6 +754,7 @@ export declare const NOTIFICATION_WEEK_NOTIFICATION_ITEM_LIMIT = 5000;
753
754
  *
754
755
  * @dbxModel
755
756
  * @dbxModelRead system
757
+ * @dbxModelServerOnly
756
758
  */
757
759
  export interface NotificationWeek {
758
760
  /**
@@ -854,6 +856,7 @@ export declare const NOTIFICATION_LOGGED_EVENT_DAY_ITEM_CONVERTER: PagedItemConv
854
856
  *
855
857
  * @dbxModel
856
858
  * @dbxModelRead system
859
+ * @dbxModelServerOnly
857
860
  */
858
861
  export interface NotificationLoggedEventDay {
859
862
  /**
@@ -68,6 +68,7 @@ export type SystemStateStoredData = Record<string, any>;
68
68
  * @dbxModel
69
69
  * @dbxModelRead system
70
70
  * @dbxModelArchetype system-state-singleton
71
+ * @dbxModelServerOnly
71
72
  */
72
73
  export interface SystemState<T extends SystemStateStoredData = SystemStateStoredData> {
73
74
  /**
package/test/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@dereekb/firebase/test",
3
- "version": "13.36.0",
3
+ "version": "13.38.0",
4
4
  "peerDependencies": {
5
- "@dereekb/date": "13.36.0",
6
- "@dereekb/firebase": "13.36.0",
7
- "@dereekb/model": "13.36.0",
8
- "@dereekb/rxjs": "13.36.0",
9
- "@dereekb/util": "13.36.0",
5
+ "@dereekb/date": "13.38.0",
6
+ "@dereekb/firebase": "13.38.0",
7
+ "@dereekb/model": "13.38.0",
8
+ "@dereekb/rxjs": "13.38.0",
9
+ "@dereekb/util": "13.38.0",
10
10
  "@firebase/rules-unit-testing": "5.0.0",
11
11
  "date-fns": "^4.1.0",
12
12
  "firebase": "^12.12.1",