@dereekb/firebase-server 13.42.0 → 13.43.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.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.42.0",
3
+ "version": "13.43.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/analytics": "13.42.0",
7
- "@dereekb/date": "13.42.0",
8
- "@dereekb/firebase": "13.42.0",
9
- "@dereekb/firebase-server": "13.42.0",
10
- "@dereekb/model": "13.42.0",
11
- "@dereekb/nestjs": "13.42.0",
12
- "@dereekb/rxjs": "13.42.0",
13
- "@dereekb/util": "13.42.0",
6
+ "@dereekb/analytics": "13.43.0",
7
+ "@dereekb/date": "13.43.0",
8
+ "@dereekb/firebase": "13.43.0",
9
+ "@dereekb/firebase-server": "13.43.0",
10
+ "@dereekb/model": "13.43.0",
11
+ "@dereekb/nestjs": "13.43.0",
12
+ "@dereekb/rxjs": "13.43.0",
13
+ "@dereekb/util": "13.43.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "archiver": "^7.0.1",
@@ -1,8 +1,7 @@
1
- import { type AppCalendarTypeConfigServiceRef, type CalendarDocument, type CalendarFirestoreCollections, type FirestoreContextReference, type FlagStaleCalendarsForSyncParams, type FlagStaleCalendarsForSyncResult, type RotateCalendarIcsParams, type RotateCalendarIcsResult, type StorageFileFirestoreCollections, type SyncAllFlaggedCalendarsParams, type SyncAllFlaggedCalendarsResult, type SyncCalendarParams, type SyncCalendarResult } from '@dereekb/firebase';
2
- import { type FirebaseServerActionsContext, type FirebaseServerStorageServiceRef } from '@dereekb/firebase-server';
1
+ import { type AppCalendarTypeConfigServiceRef, type CalendarDocument, type CalendarFirestoreCollections, type FlagStaleCalendarsForSyncParams, type FlagStaleCalendarsForSyncResult, type RotateCalendarIcsParams, type RotateCalendarIcsResult, type SyncAllFlaggedCalendarsParams, type SyncAllFlaggedCalendarsResult, type SyncCalendarParams, type SyncCalendarResult } from '@dereekb/firebase';
3
2
  import { type TransformAndValidateFunctionResult } from '@dereekb/model';
4
3
  import { type InjectionToken } from '@nestjs/common';
5
- import { type StorageFileServerActions } from '../storagefile/storagefile.action.server';
4
+ import { type BaseStorageFileServerActionsContext } from '../storagefile/storagefile.action.server';
6
5
  /**
7
6
  * NestJS injection token for the {@link BaseCalendarServerActionsContext}.
8
7
  */
@@ -14,15 +13,18 @@ export declare const CALENDAR_SERVER_ACTION_CONTEXT_TOKEN: InjectionToken;
14
13
  /**
15
14
  * Minimal context providing the Firebase infrastructure, storage and Firestore collections every Calendar
16
15
  * server action needs.
16
+ *
17
+ * Extends {@link BaseStorageFileServerActionsContext} because a Calendar's ICS feed IS a StorageFile: the
18
+ * sync and rotate paths re-flag it for processing, which builds a notification task. Taking the base
19
+ * context lets those paths build `processStorageFile` locally rather than injecting the whole
20
+ * StorageFileServerActions — and so keeps the Calendar module free of a StorageFileModule import.
17
21
  */
18
- export interface BaseCalendarServerActionsContext extends FirebaseServerActionsContext, CalendarFirestoreCollections, StorageFileFirestoreCollections, FirebaseServerStorageServiceRef, FirestoreContextReference {
22
+ export interface BaseCalendarServerActionsContext extends BaseStorageFileServerActionsContext, CalendarFirestoreCollections {
19
23
  }
20
24
  /**
21
- * Full context for the Calendar server actions, adding the type registry and the StorageFile actions the
22
- * sweep re-flags through.
25
+ * Full context for the Calendar server actions, adding the type registry.
23
26
  */
24
27
  export interface CalendarServerActionsContext extends BaseCalendarServerActionsContext, AppCalendarTypeConfigServiceRef {
25
- readonly storageFileServerActions: StorageFileServerActions;
26
28
  }
27
29
  /**
28
30
  * The publish-side server actions for the Calendar model.
@@ -2,7 +2,6 @@ import { type InjectionToken, type ModuleMetadata } from '@nestjs/common';
2
2
  import { type Maybe } from '@dereekb/util';
3
3
  import { AppCalendarTypeConfigService, type CalendarTypeConfig } from '@dereekb/firebase';
4
4
  import { type BaseCalendarServerActionsContext, CalendarServerActions, type CalendarServerActionsContext } from './calendar.action.server';
5
- import { StorageFileServerActions } from '../storagefile/storagefile.action.server';
6
5
  /**
7
6
  * NestJS injection token for the app's `CalendarTypeConfig[]` registry.
8
7
  *
@@ -25,10 +24,9 @@ export declare function appCalendarTypeConfigServiceFactory(calendarTypeConfigs:
25
24
  *
26
25
  * @param context - The base context providing Firebase infrastructure and collections.
27
26
  * @param appCalendarTypeConfigServiceInstance - The app's calendar type registry service.
28
- * @param storageFileServerActions - The StorageFile actions the sweep re-flags through.
29
27
  * @returns The fully assembled context.
30
28
  */
31
- export declare function calendarServerActionsContextFactory(context: BaseCalendarServerActionsContext, appCalendarTypeConfigServiceInstance: AppCalendarTypeConfigService, storageFileServerActions: StorageFileServerActions): CalendarServerActionsContext;
29
+ export declare function calendarServerActionsContextFactory(context: BaseCalendarServerActionsContext, appCalendarTypeConfigServiceInstance: AppCalendarTypeConfigService): CalendarServerActionsContext;
32
30
  /**
33
31
  * Factory that creates a {@link CalendarServerActions} instance from the assembled context.
34
32
  *
@@ -40,7 +38,6 @@ export interface ProvideAppCalendarMetadataConfig extends Pick<ModuleMetadata, '
40
38
  /**
41
39
  * The AppCalendarModule requires the following dependencies in order to initialize properly:
42
40
  * - BaseCalendarServerActionsContext (BASE_CALENDAR_SERVER_ACTION_CONTEXT_TOKEN)
43
- * - StorageFileServerActions
44
41
  *
45
42
  * This module declaration makes it easier to import a module that exports those dependencies.
46
43
  */
@@ -0,0 +1,192 @@
1
+ import { type AppFormSpaceTypeConfigServiceRef, type DeleteFormSpaceParams, type ExpireAllExpiredFormSpacesParams, type ExpireAllExpiredFormSpacesResult, type FirestoreContextReference, type FormSpaceDocument, type FormSpaceFirestoreCollections, type FormSpaceId, type NotificationFirestoreCollections, type ProcessAllQueuedFormSpacesParams, type ProcessAllQueuedFormSpacesResult, type RemoveFormSpaceFileParams, type StorageFileFirestoreCollections, type SubmitFormSpaceParams, type SubmitFormSpaceResult, type CreateFormSpaceParams, type UpdateFormSpaceParams } from '@dereekb/firebase';
2
+ import { type FirebaseServerActionsContext, type FirebaseServerAuthServiceRef } from '@dereekb/firebase-server';
3
+ import { type Maybe, type Milliseconds } from '@dereekb/util';
4
+ import { type TransformAndValidateFunctionResult } from '@dereekb/model';
5
+ import { type InjectionToken } from '@nestjs/common';
6
+ import { type NotificationExpediteServiceRef } from '../notification/notification.expedite.service';
7
+ /**
8
+ * NestJS injection token for the {@link BaseFormSpaceServerActionsContext}.
9
+ */
10
+ export declare const BASE_FORM_SPACE_SERVER_ACTION_CONTEXT_TOKEN: InjectionToken;
11
+ /**
12
+ * NestJS injection token for the fully assembled {@link FormSpaceServerActionsContext}.
13
+ */
14
+ export declare const FORM_SPACE_SERVER_ACTION_CONTEXT_TOKEN: InjectionToken;
15
+ /**
16
+ * Default page size for {@link expireAllExpiredFormSpacesFactory}.
17
+ */
18
+ export declare const DEFAULT_FORM_SPACE_EXPIRATION_SWEEP_PAGE_SIZE = 50;
19
+ /**
20
+ * Default wall-clock budget for {@link expireAllExpiredFormSpacesFactory}: one minute.
21
+ */
22
+ export declare const DEFAULT_FORM_SPACE_EXPIRATION_SWEEP_MAX_RUN_TIME: Milliseconds;
23
+ /**
24
+ * Minimal context providing the Firebase infrastructure and Firestore collections every FormSpace server
25
+ * action needs.
26
+ */
27
+ export interface BaseFormSpaceServerActionsContext extends FirebaseServerActionsContext, FormSpaceFirestoreCollections, StorageFileFirestoreCollections, NotificationFirestoreCollections, NotificationExpediteServiceRef, FirebaseServerAuthServiceRef, FirestoreContextReference {
28
+ }
29
+ /**
30
+ * Full context for the FormSpace server actions, adding the type registry.
31
+ */
32
+ export interface FormSpaceServerActionsContext extends BaseFormSpaceServerActionsContext, AppFormSpaceTypeConfigServiceRef {
33
+ }
34
+ /**
35
+ * Extra input for {@link FormSpaceServerActions.createFormSpace}, supplied by the callable rather than by
36
+ * the caller: the space's owner is WHO IS CALLING, never a value in the request body.
37
+ */
38
+ export interface CreateFormSpaceActionInput {
39
+ readonly uid: string;
40
+ readonly ownerKey?: Maybe<string>;
41
+ /**
42
+ * Create the space at THIS id rather than at a generated one.
43
+ *
44
+ * For the SHARED shape — one space per target model, reached by everyone who reaches the target — a
45
+ * generated id lets two concurrent callers each mint one, and neither sees the other's files. Deriving
46
+ * the id with {@link formSpaceIdForModel} makes the create idempotent and lets a client read the space
47
+ * with a plain `get` before it has ever called create.
48
+ */
49
+ readonly formSpaceId?: Maybe<FormSpaceId>;
50
+ /**
51
+ * Return the existing space when {@link formSpaceId} is already taken, instead of failing.
52
+ *
53
+ * Only meaningful alongside {@link formSpaceId} — a generated id can never collide.
54
+ */
55
+ readonly getOrCreate?: Maybe<boolean>;
56
+ }
57
+ /**
58
+ * Extra input for {@link FormSpaceServerActions.removeFormSpaceFile}, supplied by the callable rather than by
59
+ * the caller: WHO is removing decides which files they may remove, so it can never be a value in the request
60
+ * body.
61
+ */
62
+ export interface RemoveFormSpaceFileActionInput {
63
+ /**
64
+ * The uid of the caller.
65
+ *
66
+ * A REQUIRED key with a nullable value, so a call site has to state it rather than inherit an unrestricted
67
+ * remove by forgetting an optional argument. A null uid is refused by any slot whose
68
+ * {@link FormSpaceFileAccess} is `'uploader'`, which is the fail-closed direction.
69
+ */
70
+ readonly uid: Maybe<string>;
71
+ }
72
+ /**
73
+ * The server actions for the FormSpace model.
74
+ *
75
+ * @see {@link formSpaceServerActions} for the concrete implementation factory.
76
+ */
77
+ export declare abstract class FormSpaceServerActions {
78
+ abstract createFormSpace(params: CreateFormSpaceParams): Promise<TransformAndValidateFunctionResult<CreateFormSpaceParams, (input: CreateFormSpaceActionInput) => Promise<FormSpaceDocument>>>;
79
+ abstract updateFormSpace(params: UpdateFormSpaceParams): Promise<TransformAndValidateFunctionResult<UpdateFormSpaceParams, (formSpaceDocument: FormSpaceDocument) => Promise<FormSpaceDocument>>>;
80
+ abstract submitFormSpace(params: SubmitFormSpaceParams): Promise<TransformAndValidateFunctionResult<SubmitFormSpaceParams, (formSpaceDocument: FormSpaceDocument) => Promise<SubmitFormSpaceResult>>>;
81
+ abstract removeFormSpaceFile(params: RemoveFormSpaceFileParams): Promise<TransformAndValidateFunctionResult<RemoveFormSpaceFileParams, (formSpaceDocument: FormSpaceDocument, input: RemoveFormSpaceFileActionInput) => Promise<FormSpaceDocument>>>;
82
+ abstract deleteFormSpace(params: DeleteFormSpaceParams): Promise<TransformAndValidateFunctionResult<DeleteFormSpaceParams, (formSpaceDocument: FormSpaceDocument) => Promise<void>>>;
83
+ abstract processAllQueuedFormSpaces(params: ProcessAllQueuedFormSpacesParams): Promise<TransformAndValidateFunctionResult<ProcessAllQueuedFormSpacesParams, () => Promise<ProcessAllQueuedFormSpacesResult>>>;
84
+ abstract expireAllExpiredFormSpaces(params: ExpireAllExpiredFormSpacesParams): Promise<TransformAndValidateFunctionResult<ExpireAllExpiredFormSpacesParams, () => Promise<ExpireAllExpiredFormSpacesResult>>>;
85
+ }
86
+ /**
87
+ * Creates a concrete {@link FormSpaceServerActions} implementation from the given context.
88
+ *
89
+ * @param context - The fully assembled FormSpace server actions context.
90
+ * @returns The server actions.
91
+ */
92
+ export declare function formSpaceServerActions(context: FormSpaceServerActionsContext): FormSpaceServerActions;
93
+ /**
94
+ * Factory for the `createFormSpace` action.
95
+ *
96
+ * Rejects an unregistered {@link FormSpaceType} outright. This is the one place the registry is STRICT —
97
+ * a sweep over existing documents falls back to the default config so one bad document cannot take the
98
+ * pass down, but a space that could never be filled in or submitted should not be created at all.
99
+ *
100
+ * @param context - The FormSpace server actions context.
101
+ * @returns An async transform-and-validate function that creates a FormSpace.
102
+ */
103
+ export declare function createFormSpaceFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<CreateFormSpaceParams<import("@dereekb/firebase").FormSpaceData>, ({ uid, ownerKey, formSpaceId, getOrCreate }: CreateFormSpaceActionInput) => Promise<FormSpaceDocument>, object, unknown>;
104
+ /**
105
+ * Factory for the `updateFormSpace` action.
106
+ *
107
+ * `data` REPLACES the stored JSON rather than merging into it — the client owns the whole form, and a
108
+ * merge would make clearing a field impossible to express.
109
+ *
110
+ * @param context - The FormSpace server actions context.
111
+ * @returns An async transform-and-validate function that updates a draft FormSpace.
112
+ */
113
+ export declare function updateFormSpaceFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<UpdateFormSpaceParams<import("@dereekb/firebase").FormSpaceData>, (formSpaceDocument: FormSpaceDocument) => Promise<FormSpaceDocument>, object, unknown>;
114
+ /**
115
+ * Factory for the `submitFormSpace` action.
116
+ *
117
+ * The LOCK is taken in a transaction; the processing task is created afterwards. That split is deliberate:
118
+ * `createOrRunUniqueNotificationDocument()` does not accept a transaction, since running a task inside one
119
+ * would hold the lock across the whole handler. A crash in between leaves the space in
120
+ * QUEUED_FOR_PROCESSING with no task, which {@link processAllQueuedFormSpacesFactory} is the backstop for.
121
+ *
122
+ * @param context - The FormSpace server actions context.
123
+ * @returns An async transform-and-validate function that submits a FormSpace.
124
+ */
125
+ export declare function submitFormSpaceFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<SubmitFormSpaceParams, (formSpaceDocument: FormSpaceDocument) => Promise<SubmitFormSpaceResult>, object, unknown>;
126
+ /**
127
+ * Factory for the `removeFormSpaceFile` action.
128
+ *
129
+ * Drops one file from a slot. The StorageFile is FLAGGED, never deleted inline — the StorageFile delete
130
+ * sweep owns removing the object from GCS, and a second code path that removed it here is how an orphaned
131
+ * object gets left behind.
132
+ *
133
+ * `uc` is deliberately NOT decremented: it counts uploads ACCEPTED over the space's lifetime, and letting a
134
+ * remove refund it would turn `maxUploads` from a bound on work done into a bound on files retained, which
135
+ * an upload/remove loop could then evade entirely.
136
+ *
137
+ * The caller's `removeFile` ROLE opened the door; the type's {@link FormSpaceFileAccess} decides which files
138
+ * inside it are theirs. That second check is made HERE rather than in the callable because it is per-file
139
+ * and needs the space's own `f` array — the same snapshot the removal writes, read under the same
140
+ * transaction, so a concurrent supersede cannot slip a different file under the decision.
141
+ *
142
+ * @param context - The FormSpace server actions context.
143
+ * @returns An async transform-and-validate function that removes one file from a draft FormSpace.
144
+ */
145
+ export declare function removeFormSpaceFileFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<RemoveFormSpaceFileParams, (formSpaceDocument: FormSpaceDocument, input: RemoveFormSpaceFileActionInput) => Promise<FormSpaceDocument>, object, unknown>;
146
+ /**
147
+ * Creates (or re-runs) the unique submission task for a FormSpace and records its key on `pn`.
148
+ *
149
+ * Shared by `submitFormSpace` and the queued backstop sweep, which is what makes the backstop safe to run
150
+ * against a space whose task already exists: the task is unique per space, so a second attempt resolves to
151
+ * the same document rather than racing a second processor.
152
+ *
153
+ * @param context - The FormSpace server actions context.
154
+ * @returns Queues one FormSpace, reporting the task key and whether this call created it.
155
+ */
156
+ export declare function _queueFormSpaceForProcessingFactory(context: FormSpaceServerActionsContext): (formSpaceDocument: FormSpaceDocument, runImmediately?: Maybe<boolean>) => Promise<SubmitFormSpaceResult>;
157
+ /**
158
+ * Factory for the `deleteFormSpace` action.
159
+ *
160
+ * Files are FLAGGED, not deleted: the StorageFile delete sweep owns removing the object from GCS, and
161
+ * duplicating that here would mean a second code path that can leave an orphaned object behind.
162
+ *
163
+ * @param context - The FormSpace server actions context.
164
+ * @returns An async transform-and-validate function that deletes a FormSpace and flags its files.
165
+ */
166
+ export declare function deleteFormSpaceFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<DeleteFormSpaceParams, (formSpaceDocument: FormSpaceDocument) => Promise<void>, object, unknown>;
167
+ /**
168
+ * Factory for the `processAllQueuedFormSpaces` action.
169
+ *
170
+ * The BACKSTOP for a submission whose task creation was lost between the lock transaction and the task
171
+ * write. It is safe to run against a space whose task already exists because the task is unique per space.
172
+ *
173
+ * @param context - The FormSpace server actions context.
174
+ * @returns An async transform-and-validate function that returns batch processing results.
175
+ */
176
+ export declare function processAllQueuedFormSpacesFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<ProcessAllQueuedFormSpacesParams, () => Promise<ProcessAllQueuedFormSpacesResult>, object, unknown>;
177
+ /**
178
+ * Factory for the `expireAllExpiredFormSpaces` action.
179
+ *
180
+ * A PAGED sweep with a pinned cutoff and a hard time budget, copying `openRouterRunTaskExpirationSweep`.
181
+ * `firestoreDate` persists an ISO8601 string, so Firestore's native TTL cannot be pointed at `eat` — this
182
+ * sweep is the mechanism, not a stopgap.
183
+ *
184
+ * No cursor is needed and one would be meaningless: expiring a page clears its `eat`, so the page no
185
+ * longer matches and re-running the query IS the next page. An empty page is therefore the only "done"
186
+ * signal, which is why the cutoff must be pinned — a cutoff advancing with the clock would let a space
187
+ * that ages mid-sweep join a page not yet reached, making the pass unbounded.
188
+ *
189
+ * @param context - The FormSpace server actions context.
190
+ * @returns An async transform-and-validate function that expires due FormSpaces.
191
+ */
192
+ export declare function expireAllExpiredFormSpacesFactory(context: FormSpaceServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<ExpireAllExpiredFormSpacesParams, () => Promise<ExpireAllExpiredFormSpacesResult>, object, unknown>;
@@ -0,0 +1,91 @@
1
+ import { type FormSpaceFileSlot, type FormSpaceId, type FormSpaceSubmitBlocker, type FormSpaceType, type FormSpaceUploadRejectionReason } from '@dereekb/firebase';
2
+ /**
3
+ * Creates an error indicating the app never registered the requested {@link FormSpaceType}.
4
+ *
5
+ * @param formSpaceType - The unregistered type.
6
+ * @returns A bad-request HttpsError with the FORM_SPACE_TYPE_NOT_REGISTERED error code.
7
+ */
8
+ export declare function formSpaceTypeNotRegisteredError(formSpaceType: FormSpaceType): import("firebase-functions/https").HttpsError;
9
+ /**
10
+ * Creates an error indicating the FormSpace is no longer a draft, so it cannot be changed.
11
+ *
12
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_NOT_EDITABLE error code.
13
+ */
14
+ export declare function formSpaceNotEditableError(): import("firebase-functions/https").HttpsError;
15
+ /**
16
+ * Creates an error indicating a required upload slot is still empty at submission time.
17
+ *
18
+ * @param slots - The unsatisfied slots.
19
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_REQUIRED_SLOT_MISSING error code.
20
+ */
21
+ export declare function formSpaceRequiredSlotMissingError(slots: FormSpaceFileSlot[]): import("firebase-functions/https").HttpsError;
22
+ /**
23
+ * Creates an error indicating a slot holds a file that failed validation.
24
+ *
25
+ * Carries the per-file reasons rather than only the slot names: "the slot is not acceptable" is not
26
+ * actionable, "scan.png is not a readable PDF" is.
27
+ *
28
+ * @param blockers - The invalid-file blockers reported by `formSpaceSubmitBlockers`.
29
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_HAS_INVALID_FILES error code.
30
+ */
31
+ export declare function formSpaceHasInvalidFilesError(blockers: FormSpaceSubmitBlocker[]): import("firebase-functions/https").HttpsError;
32
+ /**
33
+ * Creates an error indicating a slot is still awaiting a validation verdict.
34
+ *
35
+ * @param slots - The slots still being validated.
36
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_VALIDATION_PENDING error code.
37
+ */
38
+ export declare function formSpaceValidationPendingError(slots: FormSpaceFileSlot[]): import("firebase-functions/https").HttpsError;
39
+ /**
40
+ * Creates an error indicating the slot does not hold the file that was asked to be removed.
41
+ *
42
+ * @param slot - The slot that was targeted.
43
+ * @returns A bad-request HttpsError with the FORM_SPACE_FILE_NOT_FOUND error code.
44
+ */
45
+ export declare function formSpaceFileNotFoundError(slot: FormSpaceFileSlot): import("firebase-functions/https").HttpsError;
46
+ /**
47
+ * Creates an error indicating the caller may reach the FormSpace but not this file of it.
48
+ *
49
+ * Deliberately NOT a plain forbidden: the caller passed the space-level role check, and what refused them is
50
+ * the type's `FormSpaceFileAccess` narrowing to the file's own uploader. A distinct code lets a client say
51
+ * "that is someone else's file" rather than "you cannot edit this form".
52
+ *
53
+ * @param slot - The slot holding the file.
54
+ * @returns A bad-request HttpsError with the FORM_SPACE_FILE_ACCESS_DENIED error code.
55
+ */
56
+ export declare function formSpaceFileAccessDeniedError(slot: FormSpaceFileSlot): import("firebase-functions/https").HttpsError;
57
+ /**
58
+ * Creates an error indicating the upload was rejected by the type's rules.
59
+ *
60
+ * @param reason - Why the upload was rejected.
61
+ * @returns A bad-request HttpsError with the FORM_SPACE_UPLOAD_NOT_ALLOWED error code.
62
+ */
63
+ export declare function formSpaceUploadNotAllowedError(reason: FormSpaceUploadRejectionReason): import("firebase-functions/https").HttpsError;
64
+ /**
65
+ * Creates an error indicating the uploader is not the user the FormSpace belongs to.
66
+ *
67
+ * @returns A bad-request HttpsError with the FORM_SPACE_UPLOAD_USER_MISMATCH error code.
68
+ */
69
+ export declare function formSpaceUploadUserMismatchError(): import("firebase-functions/https").HttpsError;
70
+ /**
71
+ * Creates an error indicating the referenced FormSpace does not exist.
72
+ *
73
+ * @returns An unavailable HttpsError with the FORM_SPACE_NOT_FOUND error code.
74
+ */
75
+ export declare function formSpaceNotFoundError(): import("firebase-functions/https").HttpsError;
76
+ /**
77
+ * Creates an error indicating an explicit FormSpace id is already taken.
78
+ *
79
+ * @param formSpaceId - The id that already exists.
80
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_ALREADY_EXISTS error code.
81
+ */
82
+ export declare function formSpaceAlreadyExistsError(formSpaceId: FormSpaceId): import("firebase-functions/https").HttpsError;
83
+ /**
84
+ * Creates an error indicating a get-or-create resolved to a space of a different type.
85
+ *
86
+ * @param formSpaceId - The id that resolved.
87
+ * @param expected - The type that was asked for.
88
+ * @param found - The type the existing space actually carries.
89
+ * @returns A precondition-conflict HttpsError with the FORM_SPACE_TYPE_MISMATCH error code.
90
+ */
91
+ export declare function formSpaceTypeMismatchError(formSpaceId: FormSpaceId, expected: FormSpaceType, found: FormSpaceType): import("firebase-functions/https").HttpsError;
@@ -0,0 +1,57 @@
1
+ import { type InjectionToken, type ModuleMetadata } from '@nestjs/common';
2
+ import { type Maybe } from '@dereekb/util';
3
+ import { AppFormSpaceTypeConfigService, type FormSpaceTypeConfig } from '@dereekb/firebase';
4
+ import { type BaseFormSpaceServerActionsContext, FormSpaceServerActions, type FormSpaceServerActionsContext } from './formspace.action.server';
5
+ /**
6
+ * NestJS injection token for the app's `FormSpaceTypeConfig[]` registry.
7
+ *
8
+ * Apps bind this token via {@link appFormSpaceModuleMetadata} by passing `formSpaceTypeConfigs`.
9
+ */
10
+ export declare const FORM_SPACE_TYPE_CONFIGS_TOKEN: InjectionToken;
11
+ /**
12
+ * Factory that builds the app's {@link AppFormSpaceTypeConfigService} from its registered configs.
13
+ *
14
+ * @param formSpaceTypeConfigs - The app's form space type registry.
15
+ * @returns The service.
16
+ */
17
+ export declare function appFormSpaceTypeConfigServiceFactory(formSpaceTypeConfigs: FormSpaceTypeConfig[]): AppFormSpaceTypeConfigService;
18
+ /**
19
+ * Factory that assembles the full {@link FormSpaceServerActionsContext}.
20
+ *
21
+ * @param context - The base context providing Firebase infrastructure and collections.
22
+ * @param appFormSpaceTypeConfigServiceInstance - The app's form space type registry service.
23
+ * @returns The fully assembled context.
24
+ */
25
+ export declare function formSpaceServerActionsContextFactory(context: BaseFormSpaceServerActionsContext, appFormSpaceTypeConfigServiceInstance: AppFormSpaceTypeConfigService): FormSpaceServerActionsContext;
26
+ /**
27
+ * Factory that creates a {@link FormSpaceServerActions} instance from the assembled context.
28
+ *
29
+ * @param context - The fully assembled form space server actions context.
30
+ * @returns The server actions.
31
+ */
32
+ export declare function formSpaceServerActionsFactory(context: FormSpaceServerActionsContext): FormSpaceServerActions;
33
+ export interface ProvideAppFormSpaceMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
34
+ /**
35
+ * The AppFormSpaceModule requires the following dependencies in order to initialize properly:
36
+ * - BaseFormSpaceServerActionsContext (BASE_FORM_SPACE_SERVER_ACTION_CONTEXT_TOKEN)
37
+ *
38
+ * This module declaration makes it easier to import a module that exports those dependencies.
39
+ */
40
+ readonly dependencyModule?: Maybe<Required<ModuleMetadata>['imports']['0']>;
41
+ /**
42
+ * The app's {@link FormSpaceType} registry. Bound to {@link FORM_SPACE_TYPE_CONFIGS_TOKEN}.
43
+ */
44
+ readonly formSpaceTypeConfigs: FormSpaceTypeConfig[];
45
+ }
46
+ /**
47
+ * Convenience function used to generate ModuleMetadata for an app's FormSpaceModule.
48
+ *
49
+ * By default this module exports:
50
+ * - FormSpaceServerActionsContext (FORM_SPACE_SERVER_ACTION_CONTEXT_TOKEN)
51
+ * - FormSpaceServerActions
52
+ * - AppFormSpaceTypeConfigService
53
+ *
54
+ * @param config - The module configuration.
55
+ * @returns The assembled {@link ModuleMetadata} for the form space module.
56
+ */
57
+ export declare function appFormSpaceModuleMetadata(config: ProvideAppFormSpaceMetadataConfig): ModuleMetadata;
@@ -0,0 +1,97 @@
1
+ import { type FormSpace, type FormSpaceDocument, type FormSpaceFirestoreCollections, FormSpaceProcessingState, type FormSpaceSubmissionNotificationTaskData, type FormSpaceSubmissionSubtask, type FormSpaceSubmissionSubtaskMetadata } from '@dereekb/firebase';
2
+ import { type Getter, type Maybe } from '@dereekb/util';
3
+ import { type NotificationTaskServiceTaskHandlerConfig } from '../notification/notification.task.service.handler';
4
+ import { type NotificationTaskSubtaskCleanupInstructions, type NotificationTaskSubtaskFlowEntry, type NotificationTaskSubtaskInput, type NotificationTaskSubtaskResult, type NotificationTaskSubtaskNotificationTaskHandlerConfig, type NotificationTaskSubtaskProcessorConfig } from '../notification/notification.task.subtask.handler';
5
+ /**
6
+ * @module formspace.task.service.handler
7
+ *
8
+ * The submission-processing handler: one NotificationTask type dispatching, by {@link FormSpaceType}, to
9
+ * the app's registered processors.
10
+ *
11
+ * This is a thin specialization of {@link notificationTaskSubtaskNotificationTaskHandlerFactory}, exactly
12
+ * as `storageFileProcessingNotificationTaskHandler` is — the checkpoint / retry / delay semantics are the
13
+ * framework's, and the only thing FormSpace supplies is "how to load the space" and "what to write when
14
+ * processing concludes".
15
+ */
16
+ /**
17
+ * Input handed to every FormSpace submission subtask.
18
+ *
19
+ * @template M - subtask metadata type
20
+ * @template S - subtask checkpoint string type
21
+ */
22
+ export interface FormSpaceSubmissionSubtaskInput<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> extends NotificationTaskSubtaskInput<FormSpaceSubmissionNotificationTaskData<M, S>, M, S> {
23
+ /**
24
+ * The FormSpaceDocument being processed.
25
+ */
26
+ readonly formSpaceDocument: FormSpaceDocument;
27
+ /**
28
+ * Loads the FormSpace, memoized for the duration of one task run.
29
+ *
30
+ * A getter rather than the value: a processor whose first checkpoint never touches the form data should
31
+ * not pay for a read, and a processor that touches it in three checkpoints should pay for one.
32
+ */
33
+ readonly loadFormSpace: Getter<Promise<FormSpace>>;
34
+ }
35
+ /**
36
+ * Result of a FormSpace submission subtask.
37
+ */
38
+ export type FormSpaceSubmissionSubtaskResult<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> = NotificationTaskSubtaskResult<M, S>;
39
+ /**
40
+ * One entry in a FormSpace submission processor's checkpoint flow.
41
+ */
42
+ export type FormSpaceSubmissionSubtaskFlowEntry<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> = NotificationTaskSubtaskFlowEntry<FormSpaceSubmissionSubtaskInput<M, S>, FormSpaceSubmissionNotificationTaskData<M, S>, M, S>;
43
+ /**
44
+ * What a FormSpace submission processor asks the cleanup step to write.
45
+ */
46
+ export interface FormSpaceSubmissionSubtaskCleanupOutput extends NotificationTaskSubtaskCleanupInstructions {
47
+ /**
48
+ * The processing state to leave the FormSpace in. Defaults to SUCCESS.
49
+ */
50
+ readonly nextProcessingState?: Maybe<FormSpaceProcessingState>;
51
+ /**
52
+ * Whether to move the space to ARCHIVED as part of cleanup. Defaults to false.
53
+ */
54
+ readonly archive?: Maybe<boolean>;
55
+ }
56
+ /**
57
+ * A processor for one {@link FormSpaceType}, keyed by that type as its subtask target.
58
+ *
59
+ * @template M - subtask metadata type
60
+ * @template S - subtask checkpoint string type
61
+ */
62
+ export type FormSpaceSubmissionProcessorConfig<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> = NotificationTaskSubtaskProcessorConfig<FormSpaceSubmissionSubtaskInput<M, S>, FormSpaceSubmissionSubtaskCleanupOutput, FormSpaceSubmissionNotificationTaskData<M, S>, M, S>;
63
+ /**
64
+ * Configuration for {@link formSpaceSubmissionNotificationTaskHandler}.
65
+ */
66
+ export interface FormSpaceSubmissionNotificationTaskHandlerConfig extends Omit<NotificationTaskSubtaskNotificationTaskHandlerConfig<FormSpaceSubmissionSubtaskInput, FormSpaceSubmissionSubtaskCleanupOutput, FormSpaceSubmissionNotificationTaskData>, 'processors'> {
67
+ /**
68
+ * The per-type processors.
69
+ */
70
+ readonly processors: FormSpaceSubmissionProcessorConfig[];
71
+ /**
72
+ * Accessor for the FormSpace collection.
73
+ */
74
+ readonly formSpaceFirestoreCollections: FormSpaceFirestoreCollections;
75
+ }
76
+ /**
77
+ * The cleanup instructions applied when a processor asks for none: mark the submission successful.
78
+ *
79
+ * @returns The default cleanup instructions.
80
+ */
81
+ export declare const formSpaceSubmissionNotificationTaskHandlerDefaultCleanup: () => FormSpaceSubmissionSubtaskCleanupOutput;
82
+ /**
83
+ * Creates the {@link NotificationTaskServiceTaskHandlerConfig} that processes FormSpace submissions.
84
+ *
85
+ * @param config - Handler configuration including the per-type processors and the FormSpace collection.
86
+ * @returns A NotificationTaskServiceTaskHandlerConfig wired for FormSpace submission processing.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const handler = formSpaceSubmissionNotificationTaskHandler({
91
+ * processors: [demoExampleFormSpaceProcessor],
92
+ * validate: DEMO_FORM_SPACE_TYPE_CONFIGS.map((x) => x.formSpaceType),
93
+ * formSpaceFirestoreCollections: context
94
+ * });
95
+ * ```
96
+ */
97
+ export declare function formSpaceSubmissionNotificationTaskHandler(config: FormSpaceSubmissionNotificationTaskHandlerConfig): NotificationTaskServiceTaskHandlerConfig<FormSpaceSubmissionNotificationTaskData>;
@@ -0,0 +1,89 @@
1
+ import { type FirebaseAuthUserId, type FormSpace, type FormSpaceFileSlot, type FormSpaceId } from '@dereekb/firebase';
2
+ import { type Maybe, type PromiseOrValue } from '@dereekb/util';
3
+ import { type StorageFileInitializeFromUploadServiceInitializer } from '../storagefile/storagefile.upload.service.initializer';
4
+ import { type FormSpaceServerActionsContext } from './formspace.action.server';
5
+ /**
6
+ * @module formspace.upload.initializer
7
+ *
8
+ * The AUTHORITATIVE upload gate for FormSpace files.
9
+ *
10
+ * `storage.rules` can enforce the outer size and content-type bound and can keep the write inside the
11
+ * uploader's own namespace, but it cannot read Firestore — so it cannot know whether the FormSpace exists,
12
+ * whether it is still a draft, whether this slot belongs to its type, or whether the space has already
13
+ * used up its upload budget. All of that is decided HERE, after the bytes have landed but before any
14
+ * StorageFile document exists, and a rejection returns a PERMANENT failure so the stray upload is deleted
15
+ * rather than retried forever.
16
+ */
17
+ /**
18
+ * Input handed to a {@link FormSpaceUploadAuthorizationDelegate}.
19
+ */
20
+ export interface FormSpaceUploadAuthorizationDelegateInput {
21
+ /**
22
+ * The space the bytes landed against, as loaded before any transaction.
23
+ */
24
+ readonly formSpace: FormSpace;
25
+ /**
26
+ * The space's id, as parsed from the upload path.
27
+ */
28
+ readonly formSpaceId: FormSpaceId;
29
+ /**
30
+ * The uid whose uploads folder the file landed in. Never equal to `formSpace.u` here.
31
+ */
32
+ readonly uploaderId: FirebaseAuthUserId;
33
+ /**
34
+ * The slot being filled.
35
+ */
36
+ readonly slot: FormSpaceFileSlot;
37
+ }
38
+ /**
39
+ * Decides whether an uploader who is NOT the space's own `u` may nonetheless upload into it.
40
+ *
41
+ * The DEFAULT FormSpace is single-user: `u` is set at creation, never changes, and is the only party that
42
+ * may upload into it. A SHARED space — one whose `o` names a model many users can reach — needs a second
43
+ * answer, and that answer is app policy (typically a read against some other collection), so it cannot live
44
+ * in this package.
45
+ *
46
+ * Consulted ONCE, BEFORE the claim transaction, and ONLY when `formSpace.u !== uploaderId`. Two
47
+ * consequences worth stating:
48
+ *
49
+ * - An owner's own upload never reaches the delegate, so a broken delegate cannot regress the single-user
50
+ * path every existing FormSpaceType uses.
51
+ * - Resolving it outside the transactions is safe because the decision reads only `u`, `o` and `t`, none of
52
+ * which ever change after creation. Inside `runTransaction` it would be re-run on every contention retry
53
+ * and would still read outside the transaction's snapshot, so the transaction would buy nothing.
54
+ *
55
+ * Returning `false` is a DECISION: it becomes {@link formSpaceUploadUserMismatchError}, which
56
+ * {@link FORM_SPACE_UPLOAD_REFUSAL_ERROR_CODES} already classifies as a permanent refusal, so the stray
57
+ * upload is discarded rather than retried forever. THROWING is infrastructure: it falls through to the
58
+ * transient branch and the source is left for the next sweep.
59
+ */
60
+ export type FormSpaceUploadAuthorizationDelegate = (input: FormSpaceUploadAuthorizationDelegateInput) => PromiseOrValue<boolean>;
61
+ /**
62
+ * Configuration for {@link formSpaceStorageFileUploadInitializers}.
63
+ *
64
+ * Extends the actions context rather than wrapping it, so an existing call site that passes the context
65
+ * object directly keeps compiling unchanged.
66
+ */
67
+ export interface FormSpaceStorageFileUploadInitializersConfig extends FormSpaceServerActionsContext {
68
+ /**
69
+ * Optional policy for an upload by someone other than the space's `u`.
70
+ *
71
+ * Absent by default, which is the original behaviour exactly: any uploader that is not `u` is refused.
72
+ */
73
+ readonly uploadAuthorizationDelegate?: Maybe<FormSpaceUploadAuthorizationDelegate>;
74
+ }
75
+ /**
76
+ * Creates the {@link StorageFileInitializeFromUploadServiceInitializer} entries for FormSpace uploads.
77
+ *
78
+ * ONE initializer covers every form type: the per-type rules come from the {@link FormSpaceTypeConfig}
79
+ * registry keyed off the loaded space, not from the initializer's own registration.
80
+ *
81
+ * @param config - The FormSpace server actions context, plus the optional shared-space upload policy.
82
+ * @returns The initializers to spread into the app's upload service config.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * initializer: [...appInitializers, ...formSpaceStorageFileUploadInitializers(context)]
87
+ * ```
88
+ */
89
+ export declare function formSpaceStorageFileUploadInitializers(config: FormSpaceStorageFileUploadInitializersConfig): StorageFileInitializeFromUploadServiceInitializer[];