@dereekb/firebase 13.42.0 → 14.0.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.
Files changed (33) hide show
  1. package/eslint/index.esm.js +209 -302
  2. package/eslint/package.json +9 -9
  3. package/index.esm.js +5774 -3926
  4. package/package.json +10 -10
  5. package/src/lib/common/firestore/accessor/document.rxjs.d.ts +0 -4
  6. package/src/lib/common/firestore/query/accumulator.d.ts +2 -2
  7. package/src/lib/common/firestore/query/iterator.d.ts +1 -1
  8. package/src/lib/common/model/function.d.ts +0 -14
  9. package/src/lib/model/formspace/formspace.access.d.ts +139 -0
  10. package/src/lib/model/formspace/formspace.action.d.ts +34 -0
  11. package/src/lib/model/formspace/formspace.api.d.ts +240 -0
  12. package/src/lib/model/formspace/formspace.api.error.d.ts +96 -0
  13. package/src/lib/model/formspace/formspace.d.ts +461 -0
  14. package/src/lib/model/formspace/formspace.id.d.ts +60 -0
  15. package/src/lib/model/formspace/formspace.permission.d.ts +47 -0
  16. package/src/lib/model/formspace/formspace.processing.d.ts +86 -0
  17. package/src/lib/model/formspace/formspace.query.d.ts +110 -0
  18. package/src/lib/model/formspace/formspace.task.d.ts +136 -0
  19. package/src/lib/model/formspace/formspace.type.d.ts +300 -0
  20. package/src/lib/model/formspace/formspace.upload.d.ts +204 -0
  21. package/src/lib/model/formspace/formspace.util.d.ts +514 -0
  22. package/src/lib/model/formspace/index.d.ts +13 -0
  23. package/src/lib/model/index.d.ts +1 -0
  24. package/src/lib/model/notification/notification.task.d.ts +3 -1
  25. package/src/lib/model/storagefile/storagefile.api.d.ts +19 -0
  26. package/src/lib/model/storagefile/storagefile.file.d.ts +42 -2
  27. package/src/lib/model/storagefile/storagefile.upload.d.ts +30 -0
  28. package/test/index.esm.js +158 -258
  29. package/test/package.json +8 -8
  30. package/test/src/lib/common/firebase.instance.d.ts +0 -4
  31. package/test/src/lib/common/firestore/firestore.instance.d.ts +0 -4
  32. package/test/src/lib/common/mock/mock.item.collection.fixture.d.ts +0 -7
  33. package/test/src/lib/common/storage/storage.instance.d.ts +0 -4
@@ -0,0 +1,461 @@
1
+ import { type Maybe, type SlashPathFile } from '@dereekb/util';
2
+ import { type GrantedReadRole, type GrantedUpdateRole, type GrantedDeleteRole } from '@dereekb/model';
3
+ import { AbstractFirestoreDocument, type CollectionReference, type FirestoreCollection, type FirestoreContext, type FirebaseAuthOwnershipKey, type FirebaseAuthUserId, type FirestoreModelKey } from '../../common';
4
+ import { type NotificationKey } from '../notification/notification.id';
5
+ import { type StorageFileId } from '../storagefile/storagefile.id';
6
+ import { type FormSpaceFileSlot, type FormSpaceType } from './formspace.id';
7
+ /**
8
+ * @module formspace
9
+ *
10
+ * Defines the FormSpace Firestore model: a generic, type-registered container that parks a client-side
11
+ * form's JSON while the user fills it out, accepts a bounded set of file uploads against it, and hands the
12
+ * finished result to server code that knows what to do with it.
13
+ *
14
+ * **Why one document.** The form's own values live embedded in `d` as pass-through JSON. Files are the
15
+ * thing that is actually large, and they live in GCS as ordinary {@link StorageFile}s inside a
16
+ * {@link StorageFileGroup} keyed by the FormSpace — so every existing sync / zip / delete behaviour applies
17
+ * unchanged and the document itself stays far under Firestore's 1 MiB ceiling.
18
+ *
19
+ * **Submission.** Submitting stamps `sat`, locks the space out of further edits, and moves `ps` to
20
+ * QUEUED_FOR_PROCESSING. A NotificationTask keyed by the space then dispatches to the registered
21
+ * server-side handler for its type, inheriting the checkpoint / retry / delay semantics every other task in
22
+ * the system already uses. `ps` mirrors {@link StorageFileProcessingState} field-for-field for exactly that
23
+ * reason: it is the same lifecycle.
24
+ */
25
+ /**
26
+ * Model identity for the FormSpace collection (collection name: `formSpace`, prefix: `fsp`).
27
+ *
28
+ * Deliberately not `fs`: too easy to misread against `sf` (StorageFile) in `firestore.rules`.
29
+ */
30
+ export declare const formSpaceIdentity: import("../..").RootFirestoreModelIdentity<"formSpace", "fsp">;
31
+ /**
32
+ * Lifecycle state of a {@link FormSpace}.
33
+ *
34
+ * Only DRAFT is editable. SUBMITTED is awaiting or undergoing processing, and is the one state a space can
35
+ * come BACK from: a type declaring a reopen policy lets a caller holding the `reopen` role return the space
36
+ * to DRAFT until it is fully locked. EXPIRED (retired by the sweep before it was ever submitted) and
37
+ * ARCHIVED (kept for the record after processing concluded) stay terminal — {@link isFormSpaceReopenable}
38
+ * requires SUBMITTED, so neither is reachable by a reopen.
39
+ */
40
+ export declare enum FormSpaceState {
41
+ DRAFT = 0,
42
+ SUBMITTED = 1,
43
+ EXPIRED = 2,
44
+ ARCHIVED = 3
45
+ }
46
+ /**
47
+ * Processing state of a submitted {@link FormSpace}.
48
+ *
49
+ * Mirrors {@link StorageFileProcessingState} value-for-value, because it drives the same NotificationTask
50
+ * machinery and a reader that already knows one should not have to learn a second vocabulary.
51
+ */
52
+ export declare enum FormSpaceProcessingState {
53
+ INIT_OR_NONE = 0,
54
+ QUEUED_FOR_PROCESSING = 1,
55
+ PROCESSING = 2,
56
+ FAILED = 3,
57
+ SUCCESS = 4,
58
+ DO_NOT_PROCESS = 5
59
+ }
60
+ /**
61
+ * Validation state of one {@link FormSpaceFile}.
62
+ *
63
+ * A slot that declares no validator leaves every file at NONE — "nothing to check" and "checked and fine"
64
+ * are deliberately distinct, so a type that gains a validator later does not silently inherit a pass.
65
+ */
66
+ export declare enum FormSpaceFileValidationState {
67
+ NONE = 0,
68
+ PENDING = 1,
69
+ VALID = 2,
70
+ INVALID = 3
71
+ }
72
+ /**
73
+ * Why validation could not reach a verdict about a file's CONTENT.
74
+ *
75
+ * Closed, unlike {@link FormSpaceFile.r}, because every member here is infrastructural: the object was gone,
76
+ * the file was superseded mid-check, no validator was registered, the validator threw. A content rejection's
77
+ * reason is written by the validator for a human to read and so cannot be enumerated.
78
+ */
79
+ export type FormSpaceFileValidationFailureReason = 'replaced' | 'file_unavailable' | 'no_validator' | 'error';
80
+ /**
81
+ * One file currently held in a {@link FormSpace} slot.
82
+ *
83
+ * The FormSpace's `f` array is the ONLY authority on what a space currently holds. The StorageFiles
84
+ * themselves are not queryable by the owner (`firestore.rules` grants `get` but not `list` on `/sf`) and the
85
+ * StorageFileGroup's own `f[]` is populated lazily by the group-sync sweep, so neither can answer "what is in
86
+ * this folder" at the moment an upload is accepted. This array is written in the same transaction that
87
+ * accepts the upload, so it always can.
88
+ *
89
+ * @dbxModelSubObject
90
+ */
91
+ export interface FormSpaceFile {
92
+ /**
93
+ * The slot this file fills.
94
+ *
95
+ * @dbxModelVariable slot
96
+ */
97
+ sl: FormSpaceFileSlot;
98
+ /**
99
+ * The id of the StorageFile holding the bytes.
100
+ *
101
+ * @dbxModelVariable storageFileId
102
+ */
103
+ sf: StorageFileId;
104
+ /**
105
+ * The user who put the file here.
106
+ *
107
+ * On a single-user space this always equals the space's `u`; on a SHARED one it is whichever member
108
+ * actually uploaded, which is the only thing that can answer "is this MY file". Absent on an entry written
109
+ * before the field existed, where the space's `u` was necessarily the uploader — which is exactly what
110
+ * {@link formSpaceFileUploaderId} falls back to.
111
+ *
112
+ * @dbxModelVariable uploadedBy
113
+ */
114
+ ub?: Maybe<FirebaseAuthUserId>;
115
+ /**
116
+ * The file's name, as it was uploaded.
117
+ *
118
+ * @dbxModelVariable fileName
119
+ */
120
+ n: SlashPathFile;
121
+ /**
122
+ * Validation state.
123
+ *
124
+ * @dbxModelVariable validationState
125
+ */
126
+ v: FormSpaceFileValidationState;
127
+ /**
128
+ * Free-text reason the file was judged INVALID, written for the owner to act on.
129
+ *
130
+ * @dbxModelVariable invalidReason
131
+ */
132
+ r?: Maybe<string>;
133
+ /**
134
+ * Reason validation never reached a content verdict, if it did not.
135
+ *
136
+ * @dbxModelVariable failureReason
137
+ */
138
+ fr?: Maybe<FormSpaceFileValidationFailureReason>;
139
+ /**
140
+ * The date the upload was accepted.
141
+ *
142
+ * @dbxModelVariable uploadedAt
143
+ */
144
+ at: Date;
145
+ /**
146
+ * The date validation concluded, if it has.
147
+ *
148
+ * @dbxModelVariable validatedAt
149
+ */
150
+ vat?: Maybe<Date>;
151
+ }
152
+ /**
153
+ * Firestore sub-object converter for {@link FormSpaceFile}.
154
+ *
155
+ * Dates are stored as Unix seconds rather than timestamps, matching {@link storageFileGroupEmbeddedFile}:
156
+ * these are embedded in an array that is rewritten on every upload, and the compact form keeps the document
157
+ * small enough that the array is never the reason a space approaches Firestore's ceiling.
158
+ */
159
+ export declare const formSpaceFileSubObject: import("../..").FirestoreSubObjectFieldMapFunctionsConfig<FormSpaceFile, Partial<import("@dereekb/util").ReplaceType<FormSpaceFile, import("@dereekb/util").MaybeMap<object>, any>>>;
160
+ /**
161
+ * The arbitrary JSON a FormSpace parks while the user fills the form out.
162
+ *
163
+ * PASS-THROUGH: the framework never interprets it. The type's handler is what gives it meaning, and an app
164
+ * narrows this generic to its own interface at the point it reads the space.
165
+ */
166
+ export type FormSpaceData = Record<string, unknown>;
167
+ /**
168
+ * A type-registered container for a client-side form: its in-progress JSON, its uploads, and its
169
+ * submission state.
170
+ *
171
+ * `o` drives `resourceIsOwnedByAuthOwnershipKey()` in the security rules identically to `sf` / `sfg` / `cal`,
172
+ * and `ps` / `pn` / `pat` mirror {@link StorageFile}'s processing triple exactly.
173
+ *
174
+ * @template T - the shape of the embedded form data
175
+ * @dbxModel
176
+ * @dbxModelRead owner
177
+ * @dbxModelArchetype root-entity
178
+ * @dbxModelArchetype state-machine-item
179
+ */
180
+ export interface FormSpace<T extends FormSpaceData = FormSpaceData> {
181
+ /**
182
+ * The kind of form this space holds, resolving its upload restrictions, expiration policy, and the
183
+ * server-side handler its submission is dispatched to.
184
+ *
185
+ * @dbxModelVariable formSpaceType
186
+ */
187
+ t: FormSpaceType;
188
+ /**
189
+ * Display name of the space, for the owner's own list of in-progress forms.
190
+ *
191
+ * @dbxModelVariable displayName
192
+ */
193
+ n?: Maybe<string>;
194
+ /**
195
+ * Lifecycle state. Only DRAFT is editable.
196
+ *
197
+ * @dbxModelVariable state
198
+ */
199
+ s: FormSpaceState;
200
+ /**
201
+ * Processing state of the submission.
202
+ *
203
+ * @dbxModelVariable processingState
204
+ */
205
+ ps: FormSpaceProcessingState;
206
+ /**
207
+ * The form's own values, stored as pass-through JSON.
208
+ *
209
+ * @dbxModelVariable data
210
+ */
211
+ d?: Maybe<T>;
212
+ /**
213
+ * The user the space belongs to. Set at creation and never changed.
214
+ *
215
+ * @dbxModelVariable userId
216
+ */
217
+ u: FirebaseAuthUserId;
218
+ /**
219
+ * Ownership key, if applicable. Drives read access in the security rules.
220
+ *
221
+ * @dbxModelVariable ownerKey
222
+ */
223
+ o?: Maybe<FirebaseAuthOwnershipKey>;
224
+ /**
225
+ * Key of the model this space was opened against, when it was opened against one.
226
+ *
227
+ * A TARGETING HANDLE, not an identity — several concurrent spaces may share one target, which is exactly
228
+ * why the FormSpace does not derive its own id from it.
229
+ *
230
+ * @dbxModelVariable targetModelKey
231
+ */
232
+ m?: Maybe<FirestoreModelKey>;
233
+ /**
234
+ * Monotonic count of uploads this space has ACCEPTED over its whole lifetime.
235
+ *
236
+ * NOT a live file count: superseding a slot still increments it. It exists so `maxUploads` can be
237
+ * enforced inside the same transaction that creates the StorageFile, which a query-based count could not
238
+ * do without a read of the whole collection.
239
+ *
240
+ * @dbxModelVariable uploadCount
241
+ */
242
+ uc: number;
243
+ /**
244
+ * The next index a file's permanent storage path is keyed by.
245
+ *
246
+ * NOT an index into `f`, and not a count of anything. It advances when a path is CLAIMED — before the
247
+ * bytes are copied and before the upload is accepted — so a claim that never becomes a file leaves a
248
+ * gap, which is the point. A gap costs nothing; a reused index puts two StorageFiles on one object, and
249
+ * deleting either then destroys the other's bytes.
250
+ *
251
+ * Deliberately separate from `uc`. `uc` is the upload BUDGET and must not move for a refused upload;
252
+ * this must move for every path handed out, refused or not.
253
+ *
254
+ * @dbxModelVariable nextFileIndex
255
+ */
256
+ fi: number;
257
+ /**
258
+ * Every file the space currently holds, across every slot.
259
+ *
260
+ * THE authority on the space's files. It is written in the same transaction that increments `uc`, so it is
261
+ * correct the instant an upload is accepted — which neither a StorageFile query (the owner cannot `list`
262
+ * `/sf`) nor the StorageFileGroup's lazily-synced `f[]` is.
263
+ *
264
+ * Flat rather than a map of slot to files: one array converts with one {@link firestoreObjectArray}, and
265
+ * the per-slot views callers actually want are a filter away. Bounded by the type's `maxUploads`.
266
+ *
267
+ * Distinct from `uc`: superseding a slot drops the old entry and appends a new one, leaving the length
268
+ * unchanged while `uc` still advances.
269
+ *
270
+ * @dbxModelVariable files
271
+ */
272
+ f: FormSpaceFile[];
273
+ /**
274
+ * The NotificationTask key processing this space's submission.
275
+ *
276
+ * Set when the submission is queued; cleared once processing is no longer PROCESSING.
277
+ *
278
+ * @dbxModelVariable processingNotificationKey
279
+ */
280
+ pn?: Maybe<NotificationKey>;
281
+ /**
282
+ * The date `ps` was last moved to PROCESSING. Used to detect a stuck task.
283
+ *
284
+ * @dbxModelVariable processingAt
285
+ */
286
+ pat?: Maybe<Date>;
287
+ /**
288
+ * Created at date.
289
+ *
290
+ * @dbxModelVariable createdAt
291
+ */
292
+ cat: Date;
293
+ /**
294
+ * Updated at date. Moves on every content change.
295
+ *
296
+ * @dbxModelVariable updatedAt
297
+ */
298
+ uat: Date;
299
+ /**
300
+ * The date the CURRENT submission was made, if the space is submitted. Its presence IS the lock.
301
+ *
302
+ * Cleared by a reopen, which is what hands the space back as an editable draft, so it always describes
303
+ * the submission in force NOW rather than the history. `fsat` is what remembers the first one.
304
+ *
305
+ * @dbxModelVariable submittedAt
306
+ */
307
+ sat?: Maybe<Date>;
308
+ /**
309
+ * The date the space was FIRST submitted, if it ever was.
310
+ *
311
+ * Never cleared. Since a reopen clears `sat`, without this the fact that the space was submitted at all —
312
+ * and when — would be destroyed by the first reopen. It is also the anchor
313
+ * {@link resolveFormSpaceLocksAt} measures the type's `reopenableUntil` from, which is what stops a
314
+ * reopen/resubmit round from walking the lock deadline forward.
315
+ *
316
+ * @dbxModelVariable firstSubmittedAt
317
+ */
318
+ fsat?: Maybe<Date>;
319
+ /**
320
+ * The date processing of the submission concluded, if it has.
321
+ *
322
+ * @dbxModelVariable completedAt
323
+ */
324
+ cpat?: Maybe<Date>;
325
+ /**
326
+ * The date this space becomes eligible for the expiration sweep, if it expires at all.
327
+ *
328
+ * CLEARED whenever the space leaves the expirable window — on submit, and on expiry itself. That is what
329
+ * keeps {@link formSpacesDueForExpirationQuery} on a single-field inequality: Firestore skips a document
330
+ * where the field is absent, so a cleared `eat` removes the space from the sweep entirely.
331
+ *
332
+ * @dbxModelVariable expiresAt
333
+ */
334
+ eat?: Maybe<Date>;
335
+ /**
336
+ * The date reopening stops being possible — the instant the submission becomes FULLY LOCKED.
337
+ *
338
+ * Written once, on the first submit, as `fsat + reopenableUntil`, and never moved afterwards; an explicit
339
+ * lock sets it to that moment instead. Absent means there is no CEILING, not that the space is locked:
340
+ * the type's `reopenableFor` is the master switch, and a type declaring neither is simply never
341
+ * reopenable. The same "an absent field is an absent gate" convention `eat` uses.
342
+ *
343
+ * Stored as an ISO8601 string like every other date here, which means `firestore.rules` CANNOT compare it
344
+ * against `request.time` — rules convert only bool/int/float/null to a string, never a timestamp. A
345
+ * downstream app that needs the lock predicate inside its OWN rules has to denormalize a unix-seconds
346
+ * mirror onto its own model and compare that. Reading this field over a callable, or over the `get` the
347
+ * rules already grant, needs no mirror at all.
348
+ *
349
+ * @dbxModelVariable locksAt
350
+ */
351
+ lat?: Maybe<Date>;
352
+ /**
353
+ * The user who locked the submission early, when a caller did rather than the deadline passing.
354
+ *
355
+ * @dbxModelVariable lockedBy
356
+ */
357
+ lby?: Maybe<FirebaseAuthUserId>;
358
+ /**
359
+ * Monotonic count of times this space has been REOPENED after a submission.
360
+ *
361
+ * Doubles as the submission-attempt generation. The submission task is keyed by it, so a resubmit gets a
362
+ * fresh task instead of colliding with the finished one, and a task still carrying a stale count is
363
+ * fenced off rather than clobbering the attempt in force.
364
+ *
365
+ * Monotonic for the same reason `uc` is: it counts rounds that happened, and a counter something can
366
+ * rewind is not a bound. Capped by the type's `maxReopens`.
367
+ *
368
+ * @dbxModelVariable reopenCount
369
+ */
370
+ rc: number;
371
+ /**
372
+ * The date the space was last reopened, if it ever was.
373
+ *
374
+ * @dbxModelVariable reopenedAt
375
+ */
376
+ rat?: Maybe<Date>;
377
+ /**
378
+ * The user who last reopened the space.
379
+ *
380
+ * @dbxModelVariable reopenedBy
381
+ */
382
+ rby?: Maybe<FirebaseAuthUserId>;
383
+ }
384
+ /**
385
+ * Permission roles for FormSpace operations.
386
+ *
387
+ * `submit` is separate from `update` because it is the one-way door: an owner who may edit a draft is not
388
+ * necessarily the party allowed to finalize it.
389
+ *
390
+ * `uploadFile` and `removeFile` are separate from `update` for the mirror-image reason. On a SHARED space a
391
+ * member contributes files to a form whose `d` belongs to everyone; letting them add or take back their own
392
+ * file must not also let them rewrite the form. WHICH files a `removeFile` holder may remove is a second,
393
+ * per-file question the type's {@link FormSpaceFileAccess} answers — the role only opens the door.
394
+ *
395
+ * The two are enforced in DIFFERENT places, because an upload and a removal arrive by different routes.
396
+ * `removeFile` gates a callable, so a role map answers it directly. An upload has no callable at all — the
397
+ * client writes bytes into its own storage namespace and a storage trigger picks them up with no auth
398
+ * context to build a role map from — so `uploadFile` is the DECLARATION of who may contribute, and
399
+ * `FormSpaceUploadAuthorizationDelegate` is where the same app policy is actually applied. Grant them
400
+ * together, to the same people.
401
+ *
402
+ * `reopen` and `lock` are the two halves of undoing that door, and are separate from each other as much as
403
+ * from `submit`. `reopen` returns a submitted space to DRAFT; WHETHER it may be reopened at all is the
404
+ * type's own policy, re-asserted inside the action's transaction, so the role only says who is allowed to
405
+ * ask. `lock` goes the other way and ends the reopen window early — in a two-party flow that is a
406
+ * privilege the party who submitted should not automatically hold over the party reviewing, which is
407
+ * exactly why it is not folded into `reopen`.
408
+ */
409
+ export type FormSpaceRoles = GrantedReadRole | GrantedUpdateRole | GrantedDeleteRole | 'submit' | 'uploadFile' | 'removeFile' | 'reopen' | 'lock';
410
+ /**
411
+ * Firestore document wrapper for a {@link FormSpace}.
412
+ *
413
+ * Deliberately NOT generic over the form data. The converter and the collection are both typed to the base
414
+ * {@link FormSpace}, so a generic here would be a type-level claim nothing downstream could honour — a
415
+ * caller that knows its type's shape narrows `d` at the read site instead.
416
+ */
417
+ export declare class FormSpaceDocument extends AbstractFirestoreDocument<FormSpace, FormSpaceDocument, typeof formSpaceIdentity> {
418
+ get modelIdentity(): import("../..").RootFirestoreModelIdentity<"formSpace", "fsp">;
419
+ }
420
+ /**
421
+ * Snapshot converter for {@link FormSpace} documents.
422
+ */
423
+ export declare const formSpaceConverter: import("../..").SnapshotConverterFunctions<FormSpace<FormSpaceData>, Partial<import("@dereekb/util").ReplaceType<FormSpace<FormSpaceData>, import("@dereekb/util").MaybeMap<object>, any>>>;
424
+ /**
425
+ * Returns the raw Firestore CollectionReference for the FormSpace collection.
426
+ *
427
+ * @param context - The Firestore context to use.
428
+ * @returns The CollectionReference for FormSpace documents.
429
+ */
430
+ export declare function formSpaceCollectionReference(context: FirestoreContext): CollectionReference<FormSpace>;
431
+ /**
432
+ * Typed FirestoreCollection for {@link FormSpace} documents.
433
+ */
434
+ export type FormSpaceFirestoreCollection = FirestoreCollection<FormSpace, FormSpaceDocument>;
435
+ /**
436
+ * Creates a fully configured {@link FormSpaceFirestoreCollection} with snapshot conversion and document factory.
437
+ *
438
+ * @param firestoreContext - The Firestore context to use.
439
+ * @returns A configured FormSpaceFirestoreCollection.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const collection = formSpaceFirestoreCollection(firestoreContext);
444
+ * const doc = collection.documentAccessor().loadDocumentForId(formSpaceId);
445
+ * ```
446
+ */
447
+ export declare function formSpaceFirestoreCollection(firestoreContext: FirestoreContext): FormSpaceFirestoreCollection;
448
+ /**
449
+ * Abstract base providing access to the FormSpace Firestore collection.
450
+ *
451
+ * Implement this in your app module to wire up the collection for dependency injection.
452
+ *
453
+ * @dbxModelGroup FormSpace
454
+ */
455
+ export declare abstract class FormSpaceFirestoreCollections {
456
+ abstract readonly formSpaceCollection: FormSpaceFirestoreCollection;
457
+ }
458
+ /**
459
+ * Union of all FormSpace-related model identity types.
460
+ */
461
+ export type FormSpaceTypes = typeof formSpaceIdentity;
@@ -0,0 +1,60 @@
1
+ import { type FirestoreModelId, type FirestoreModelKey, twoWayFlatFirestoreModelKey } from '../../common';
2
+ /**
3
+ * @module formspace.id
4
+ *
5
+ * Identity types for the FormSpace model.
6
+ *
7
+ * A FormSpace has an ARBITRARY, auto-generated id rather than an id derived from another model's key: a
8
+ * user may hold several spaces of the same type against the same target at once (a resubmission, a second
9
+ * application), so a derived id would make two concurrent drafts collide. The association to another model
10
+ * is carried by the optional `m` (targetModelKey) field instead.
11
+ */
12
+ /**
13
+ * Firestore document id for a FormSpace. Auto-generated.
14
+ */
15
+ export type FormSpaceId = FirestoreModelId;
16
+ /**
17
+ * Full Firestore document key (collection path + id) for a FormSpace.
18
+ */
19
+ export type FormSpaceKey = FirestoreModelKey;
20
+ /**
21
+ * The {@link FormSpaceId} of a space whose identity IS the model it was opened against.
22
+ *
23
+ * `formSpaceIdForModel('gb/abc123') // 'gb_abc123'`
24
+ *
25
+ * The DEFAULT shape is the one this module's own doc describes: an arbitrary id, because several concurrent
26
+ * drafts may target one model. This is the other shape — a space SHARED by everyone who reaches the target,
27
+ * where "one space per target" is the identity itself. There, a generated id is a bug rather than a
28
+ * convenience: two callers bootstrapping at once each mint a "shared" space, and neither sees the other's
29
+ * files. Deriving the id makes the create idempotent and lets a client read the space with a plain `get`
30
+ * before it has ever called create.
31
+ *
32
+ * Same construction as {@link calendarIdForModel}, and for the same reason.
33
+ */
34
+ export declare const formSpaceIdForModel: typeof twoWayFlatFirestoreModelKey;
35
+ /**
36
+ * Arbitrary string describing the kind of form a FormSpace holds, driving its upload restrictions, its
37
+ * expiration policy, and the server-side handler its submission is dispatched to through the app's
38
+ * {@link FormSpaceTypeConfig} registry.
39
+ *
40
+ * Open by design, exactly like {@link StorageFilePurpose} and {@link CalendarType}: a downstream app
41
+ * registers its own types without a library change.
42
+ *
43
+ * @semanticType
44
+ * @semanticTopic identifier
45
+ * @semanticTopic string
46
+ * @semanticTopic dereekb-firebase:form-space
47
+ */
48
+ export type FormSpaceType = string;
49
+ /**
50
+ * Names one upload "slot" within a FormSpace — the resume, the cover letter, the photo of the ID.
51
+ *
52
+ * A slot is stored on the uploaded {@link StorageFile} as its `pg` (purposeSubgroup), which is what makes
53
+ * "replace the file in this slot" the existing `flagPreviousForDelete` behaviour rather than new machinery.
54
+ *
55
+ * @semanticType
56
+ * @semanticTopic identifier
57
+ * @semanticTopic string
58
+ * @semanticTopic dereekb-firebase:form-space
59
+ */
60
+ export type FormSpaceFileSlot = string;
@@ -0,0 +1,47 @@
1
+ import { type Getter, type Maybe } from '@dereekb/util';
2
+ import { type FirebaseModelContext, type FirebasePermissionServiceModel, type FirestoreModelKey, type GrantRolesOtherwiseFunction, type GrantedRolesOtherwiseFunctionResult } from '../../common';
3
+ import { type FormSpace, type FormSpaceDocument, type FormSpaceRoles } from './formspace';
4
+ /**
5
+ * Configuration for {@link grantFormSpaceRolesForUserAuthFunction}, providing the permission
6
+ * service output, auth context, and target FormSpace document.
7
+ */
8
+ export interface GrantFormSpaceRolesForUserAuthFunctionConfig<T extends FirebaseModelContext> {
9
+ readonly output: FirebasePermissionServiceModel<FormSpace, FormSpaceDocument>;
10
+ readonly context: T;
11
+ readonly model: FormSpaceDocument;
12
+ }
13
+ /**
14
+ * Input for the role granting function, specifying which roles to grant based on
15
+ * user ownership and/or ownership key matching.
16
+ */
17
+ export interface GrantFormSpaceRolesForUserAuthInput {
18
+ /**
19
+ * Roles to grant if the user matches the FormSpace's `u` value.
20
+ */
21
+ readonly rolesForFormSpaceUser?: Maybe<Getter<GrantedRolesOtherwiseFunctionResult<FormSpaceRoles>>>;
22
+ /**
23
+ * Roles to grant if the FormSpace carries an ownership key.
24
+ */
25
+ readonly rolesForFormSpaceOwnershipKey?: Maybe<(ownershipKey: FirestoreModelKey) => GrantedRolesOtherwiseFunctionResult<FormSpaceRoles>>;
26
+ }
27
+ export type GrantFormSpaceRolesForUserAuthFunction = (input: GrantFormSpaceRolesForUserAuthInput) => GrantRolesOtherwiseFunction<FormSpaceRoles>;
28
+ /**
29
+ * Creates a function that grants {@link FormSpaceRoles} based on the authentication context.
30
+ *
31
+ * Mirrors {@link grantStorageFileRolesForUserAuthFunction}: the two conditions — the caller IS the space's
32
+ * user, and the space carries an ownership key the caller satisfies — are evaluated in parallel and merged.
33
+ *
34
+ * @param config - Permission output, auth context, and target document for the grant.
35
+ * @returns Builder that takes role configuration and yields a GrantRolesOtherwiseFunction.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const grantRoles = grantFormSpaceRolesForUserAuthFunction({ output, context, model });
40
+ * const otherwise = grantRoles({
41
+ * rolesForFormSpaceUser: () => ({ read: true, update: true, submit: true, delete: true })
42
+ * });
43
+ * ```
44
+ *
45
+ * @__NO_SIDE_EFFECTS__
46
+ */
47
+ export declare function grantFormSpaceRolesForUserAuthFunction<T extends FirebaseModelContext>(config: GrantFormSpaceRolesForUserAuthFunctionConfig<T>): GrantFormSpaceRolesForUserAuthFunction;
@@ -0,0 +1,86 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type StorageFileMetadata } from '../storagefile/storagefile.id';
3
+ import { type StorageFileProcessingSubtask, type StorageFileProcessingSubtaskMetadata } from '../storagefile/storagefile.task';
4
+ import { type FormSpaceFileValidationFailureReason } from './formspace';
5
+ import { type FormSpaceFileSlot } from './formspace.id';
6
+ /**
7
+ * @module formspace.processing
8
+ *
9
+ * The subtask vocabulary and StorageFile metadata for validating a file uploaded into a FormSpace.
10
+ *
11
+ * FormSpace validation rides the EXISTING `SFP` storage-file processing task rather than a task type of its
12
+ * own: a validated attachment is a StorageFile being processed, and the retry, stuck-detection, delay, and
13
+ * cleanup behaviour it inherits is the whole reason the purpose-processor mechanism exists. The processor
14
+ * that consumes these lives server-side, in `@dereekb/firebase-server/model`.
15
+ */
16
+ /**
17
+ * The first checkpoint: make sure the FormSpace knows this file exists.
18
+ *
19
+ * The upload initializer already writes the entry, in the same transaction that increments `uc` — that is
20
+ * what makes `maxFiles` enforceable and what gives the owner an entry the moment the upload is accepted.
21
+ * This step RECONCILES: it re-adds an entry for a StorageFile that carries this purpose but is missing from
22
+ * its space, which is the case for a file created by any path other than that initializer.
23
+ *
24
+ * It runs before validation so a reconciled file is validated in the same task rather than waiting for the
25
+ * next sweep to notice it.
26
+ */
27
+ export declare const FORM_SPACE_PURPOSE_REGISTER_SUBTASK: StorageFileProcessingSubtask;
28
+ /**
29
+ * The second checkpoint: run the slot's registered validator.
30
+ *
31
+ * One checkpoint rather than a `send`/`retrieve` pair like the resume check: a validator that needs to wait
32
+ * on something returns a `pending` verdict with a retry delay, which re-enters this same checkpoint. A
33
+ * second checkpoint would only add a state the validator cannot see.
34
+ */
35
+ export declare const FORM_SPACE_PURPOSE_VALIDATE_SUBTASK: StorageFileProcessingSubtask;
36
+ /**
37
+ * Type alias for the FormSpace file processing checkpoints.
38
+ */
39
+ export type FormSpaceFileValidationSubtask = typeof FORM_SPACE_PURPOSE_REGISTER_SUBTASK | typeof FORM_SPACE_PURPOSE_VALIDATE_SUBTASK;
40
+ /**
41
+ * Metadata carried between runs of the FormSpace file validation subtask.
42
+ *
43
+ * The verdict is recorded here as well as on the FormSpace because the cleanup step — which is what writes
44
+ * the StorageFile's final processing state — runs after the flow and can only see the persisted metadata.
45
+ */
46
+ export interface FormSpaceFileValidationSubtaskMetadata extends StorageFileProcessingSubtaskMetadata {
47
+ /**
48
+ * The slot the file fills, copied from the StorageFile on the first run.
49
+ */
50
+ readonly slot?: Maybe<FormSpaceFileSlot>;
51
+ /**
52
+ * Whether the concluded verdict judged the file valid.
53
+ */
54
+ readonly valid?: Maybe<boolean>;
55
+ /**
56
+ * Free-text reason the file was judged invalid.
57
+ */
58
+ readonly reason?: Maybe<string>;
59
+ /**
60
+ * Reason no content verdict was reached.
61
+ */
62
+ readonly failureReason?: Maybe<FormSpaceFileValidationFailureReason>;
63
+ /**
64
+ * How many times the validator has been asked for a verdict.
65
+ */
66
+ readonly attempts?: Maybe<number>;
67
+ /**
68
+ * Whether the register step had to add the file back to its FormSpace.
69
+ *
70
+ * Normally false — the upload initializer already registered it. A true here means the file reached
71
+ * processing without its space knowing about it, which is worth being able to see after the fact.
72
+ */
73
+ readonly reconciled?: Maybe<boolean>;
74
+ }
75
+ /**
76
+ * Metadata written onto a validated FormSpace file's StorageFile.
77
+ *
78
+ * Mirrors what lands on the FormSpace, so a StorageFile inspected on its own still explains itself. A
79
+ * validator that wants to record more (extracted dates, a page count) returns its own metadata, which
80
+ * REPLACES this rather than merging — the validator owns the shape it needs downstream.
81
+ */
82
+ export interface FormSpaceFileValidationStorageFileMetadata extends StorageFileMetadata {
83
+ readonly valid: boolean;
84
+ readonly reason?: Maybe<string>;
85
+ readonly checkedAt: Date;
86
+ }