@dereekb/firebase 13.41.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.
- package/eslint/index.esm.js +145 -170
- package/eslint/package.json +3 -3
- package/index.esm.js +7207 -2997
- package/package.json +5 -5
- package/src/lib/common/firestore/snapshot/snapshot.field.d.ts +4 -0
- package/src/lib/common/storage/context.d.ts +9 -1
- package/src/lib/common/storage/driver/accessor.d.ts +12 -0
- package/src/lib/common/storage/index.d.ts +1 -0
- package/src/lib/common/storage/storage.url.d.ts +69 -0
- package/src/lib/model/calendar/calendar.action.d.ts +34 -0
- package/src/lib/model/calendar/calendar.api.d.ts +147 -0
- package/src/lib/model/calendar/calendar.api.error.d.ts +24 -0
- package/src/lib/model/calendar/calendar.d.ts +460 -0
- package/src/lib/model/calendar/calendar.expand.d.ts +95 -0
- package/src/lib/model/calendar/calendar.ics.d.ts +322 -0
- package/src/lib/model/calendar/calendar.id.d.ts +110 -0
- package/src/lib/model/calendar/calendar.processing.d.ts +77 -0
- package/src/lib/model/calendar/calendar.query.d.ts +76 -0
- package/src/lib/model/calendar/calendar.schedule.d.ts +82 -0
- package/src/lib/model/calendar/calendar.type.d.ts +188 -0
- package/src/lib/model/calendar/calendar.util.d.ts +485 -0
- package/src/lib/model/calendar/index.d.ts +12 -0
- package/src/lib/model/formspace/formspace.access.d.ts +139 -0
- package/src/lib/model/formspace/formspace.action.d.ts +34 -0
- package/src/lib/model/formspace/formspace.api.d.ts +216 -0
- package/src/lib/model/formspace/formspace.api.error.d.ts +71 -0
- package/src/lib/model/formspace/formspace.d.ts +390 -0
- package/src/lib/model/formspace/formspace.id.d.ts +60 -0
- package/src/lib/model/formspace/formspace.permission.d.ts +47 -0
- package/src/lib/model/formspace/formspace.processing.d.ts +86 -0
- package/src/lib/model/formspace/formspace.query.d.ts +110 -0
- package/src/lib/model/formspace/formspace.task.d.ts +75 -0
- package/src/lib/model/formspace/formspace.type.d.ts +267 -0
- package/src/lib/model/formspace/formspace.upload.d.ts +204 -0
- package/src/lib/model/formspace/formspace.util.d.ts +348 -0
- package/src/lib/model/formspace/index.d.ts +13 -0
- package/src/lib/model/index.d.ts +2 -0
- package/src/lib/model/notification/notification.message.d.ts +83 -0
- package/src/lib/model/notification/notification.query.d.ts +41 -0
- package/src/lib/model/oidcmodel/oidcmodel.query.d.ts +36 -0
- package/src/lib/model/storagefile/storagefile.api.d.ts +42 -2
- package/src/lib/model/storagefile/storagefile.create.d.ts +14 -3
- package/src/lib/model/storagefile/storagefile.file.d.ts +42 -2
- package/src/lib/model/storagefile/storagefile.query.d.ts +36 -0
- package/src/lib/model/storagefile/storagefile.upload.d.ts +30 -0
- package/src/lib/model/system/index.d.ts +1 -0
- package/src/lib/model/system/system.scheduler.d.ts +235 -0
- package/test/index.esm.js +24 -1
- package/test/package.json +6 -6
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type FirestoreQueryConstraint } from '../../common/firestore/query/constraint';
|
|
3
|
+
import { type FirebaseAuthOwnershipKey } from '../../common/auth/auth';
|
|
4
|
+
import { type FormSpaceKey } from './formspace.id';
|
|
5
|
+
/**
|
|
6
|
+
* @module formspace.query
|
|
7
|
+
*
|
|
8
|
+
* SINGLE-FIELD BY CONSTRUCTION. `firestore.indexes.json` is generated from downstream `-firebase`
|
|
9
|
+
* components and cannot resolve an identity declared upstream here, so a FormSpace query that needed a
|
|
10
|
+
* composite index would have no way to declare one. Every query below is therefore one field plus an
|
|
11
|
+
* optional sort on that same field, which Firestore serves from its automatic single-field indexes.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Returns query constraints for FormSpaces awaiting a processing task (`ps == QUEUED_FOR_PROCESSING`).
|
|
15
|
+
*
|
|
16
|
+
* This is the backstop sweep: submission normally creates the task inline, so a space that is still sitting
|
|
17
|
+
* here is one whose task creation was lost.
|
|
18
|
+
*
|
|
19
|
+
* @param limitCount - Maximum number of results. Omit for unbounded.
|
|
20
|
+
* @returns Firestore query constraints for FormSpaces queued for processing.
|
|
21
|
+
*
|
|
22
|
+
* @dbxModelFirebaseIndex
|
|
23
|
+
* @dbxModelFirebaseIndexModel FormSpace
|
|
24
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
25
|
+
* @dbxModelFirebaseIndexCategory sweep
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const constraints = formSpacesQueuedForProcessingQuery(100);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function formSpacesQueuedForProcessingQuery(limitCount?: Maybe<number>): FirestoreQueryConstraint[];
|
|
33
|
+
/**
|
|
34
|
+
* Input for {@link formSpacesDueForExpirationQuery}.
|
|
35
|
+
*/
|
|
36
|
+
export interface FormSpacesDueForExpirationQueryInput {
|
|
37
|
+
/**
|
|
38
|
+
* FormSpaces whose `eat` is at or before this instant are due.
|
|
39
|
+
*
|
|
40
|
+
* PIN THIS for the whole sweep rather than re-deriving it per page — a cutoff that advances with the
|
|
41
|
+
* clock lets a space that ages mid-sweep join a page not yet reached, making the pass unbounded.
|
|
42
|
+
*/
|
|
43
|
+
readonly before: Date;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum number of results per page.
|
|
46
|
+
*/
|
|
47
|
+
readonly limit?: Maybe<number>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns query constraints for FormSpaces whose expiration instant has arrived.
|
|
51
|
+
*
|
|
52
|
+
* NOTE: a Firestore inequality skips documents where the field is absent, so a space with no `eat` — one
|
|
53
|
+
* whose type does not expire, or one that has already been submitted or expired and had `eat` cleared — is
|
|
54
|
+
* not matched. That absence IS the exclusion mechanism; there is no second flag to keep in step.
|
|
55
|
+
*
|
|
56
|
+
* @param input - The pinned cutoff and page size.
|
|
57
|
+
* @returns Firestore query constraints for FormSpaces due for expiration.
|
|
58
|
+
*
|
|
59
|
+
* @dbxModelFirebaseIndex
|
|
60
|
+
* @dbxModelFirebaseIndexModel FormSpace
|
|
61
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
62
|
+
* @dbxModelFirebaseIndexCategory cleanup
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const constraints = formSpacesDueForExpirationQuery({ before: new Date(), limit: 50 });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function formSpacesDueForExpirationQuery(input: FormSpacesDueForExpirationQueryInput): FirestoreQueryConstraint[];
|
|
70
|
+
/**
|
|
71
|
+
* Returns query constraints for every StorageFile that belongs to a FormSpace.
|
|
72
|
+
*
|
|
73
|
+
* A single `array-contains` on the group id, which Firestore serves from its automatic array index — the
|
|
74
|
+
* FormSpace's own group id is the only handle needed, so nothing here requires a composite index.
|
|
75
|
+
*
|
|
76
|
+
* @param formSpaceKey - The FormSpace whose files to select.
|
|
77
|
+
* @returns Firestore query constraints for the space's StorageFiles.
|
|
78
|
+
*
|
|
79
|
+
* @dbxModelFirebaseIndex
|
|
80
|
+
* @dbxModelFirebaseIndexModel StorageFile
|
|
81
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
82
|
+
* @dbxModelFirebaseIndexCategory lookup
|
|
83
|
+
* @dbxModelFirebaseIndexSkip true
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const constraints = storageFilesForFormSpaceQuery('fsp/abc123');
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function storageFilesForFormSpaceQuery(formSpaceKey: FormSpaceKey): FirestoreQueryConstraint[];
|
|
91
|
+
/**
|
|
92
|
+
* Returns query constraints for every FormSpace carrying the given ownership key.
|
|
93
|
+
*
|
|
94
|
+
* Ordering is deliberately left to the caller/client: a second field here would need a composite index this
|
|
95
|
+
* package has no way to declare.
|
|
96
|
+
*
|
|
97
|
+
* @param ownerKey - The ownership key to filter by.
|
|
98
|
+
* @returns Firestore query constraints for the owner's FormSpaces.
|
|
99
|
+
*
|
|
100
|
+
* @dbxModelFirebaseIndex
|
|
101
|
+
* @dbxModelFirebaseIndexModel FormSpace
|
|
102
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
103
|
+
* @dbxModelFirebaseIndexCategory lookup
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const constraints = formSpacesForOwnerQuery('pr/abc123');
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function formSpacesForOwnerQuery(ownerKey: FirebaseAuthOwnershipKey): FirestoreQueryConstraint[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type CreateNotificationTaskTemplate } from '../notification/notification.create.task';
|
|
3
|
+
import { type NotificationTaskSubtaskCheckpointString, type NotificationTaskSubtaskData, type NotificationTaskSubtaskMetadata } from '../notification/notification.task.subtask';
|
|
4
|
+
import { type NotificationTaskType } from '../notification/notification.id';
|
|
5
|
+
import { type FormSpaceDocument } from './formspace';
|
|
6
|
+
import { type FormSpaceId, type FormSpaceType } from './formspace.id';
|
|
7
|
+
/**
|
|
8
|
+
* @module formspace.task
|
|
9
|
+
*
|
|
10
|
+
* The NotificationTask that processes a submitted FormSpace.
|
|
11
|
+
*
|
|
12
|
+
* ONE task type for every form type. The task's SUBTASK TARGET is the {@link FormSpaceType}, so a new form
|
|
13
|
+
* type registers a processor rather than a task type — the same specialization
|
|
14
|
+
* `StorageFileProcessingNotificationTask` already does by purpose.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* NotificationTask type identifier for FormSpace submission processing.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FORM_SPACE_SUBMISSION_NOTIFICATION_TASK_TYPE: NotificationTaskType;
|
|
20
|
+
/**
|
|
21
|
+
* Checkpoint string for a FormSpace submission subtask.
|
|
22
|
+
*/
|
|
23
|
+
export type FormSpaceSubmissionSubtask = NotificationTaskSubtaskCheckpointString;
|
|
24
|
+
/**
|
|
25
|
+
* Arbitrary metadata carried between a FormSpace submission's subtasks.
|
|
26
|
+
*/
|
|
27
|
+
export type FormSpaceSubmissionSubtaskMetadata = NotificationTaskSubtaskMetadata;
|
|
28
|
+
/**
|
|
29
|
+
* Data payload for a FormSpace submission NotificationTask.
|
|
30
|
+
*
|
|
31
|
+
* @template M - subtask metadata type
|
|
32
|
+
* @template S - subtask checkpoint string type
|
|
33
|
+
*/
|
|
34
|
+
export interface FormSpaceSubmissionNotificationTaskData<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> extends NotificationTaskSubtaskData<M, S> {
|
|
35
|
+
/**
|
|
36
|
+
* The FormSpaceDocument id.
|
|
37
|
+
*/
|
|
38
|
+
readonly formSpace: FormSpaceId;
|
|
39
|
+
/**
|
|
40
|
+
* The FormSpace's type, which is also the subtask target.
|
|
41
|
+
*
|
|
42
|
+
* Retrieved from the FormSpace the first time the task runs and re-copied onto the metadata afterwards,
|
|
43
|
+
* so subsequent runs do not re-read the document only to learn which processor to dispatch to.
|
|
44
|
+
*/
|
|
45
|
+
readonly t?: Maybe<FormSpaceType>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Input for {@link formSpaceSubmissionNotificationTaskTemplate}.
|
|
49
|
+
*/
|
|
50
|
+
export interface FormSpaceSubmissionNotificationTaskInput<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata> extends Omit<FormSpaceSubmissionNotificationTaskData<M>, 'formSpace' | 't' | 'sfps'> {
|
|
51
|
+
readonly formSpaceDocument: FormSpaceDocument;
|
|
52
|
+
readonly overrideExistingTask?: Maybe<boolean>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Creates a {@link CreateNotificationTaskTemplate} for a FormSpace submission task.
|
|
56
|
+
*
|
|
57
|
+
* The task is UNIQUE to the FormSpace: a space submits once, and a second template for the same space must
|
|
58
|
+
* resolve to the same document rather than racing a second processor against the first.
|
|
59
|
+
*
|
|
60
|
+
* @param input - The target FormSpaceDocument and optional subtask data.
|
|
61
|
+
* @returns A CreateNotificationTaskTemplate for the submission task.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const template = formSpaceSubmissionNotificationTaskTemplate({ formSpaceDocument: doc });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function formSpaceSubmissionNotificationTaskTemplate(input: FormSpaceSubmissionNotificationTaskInput): CreateNotificationTaskTemplate;
|
|
69
|
+
/**
|
|
70
|
+
* All NotificationTask types used by the FormSpace system.
|
|
71
|
+
*
|
|
72
|
+
* Register these with the notification task service so an unhandled type is caught at wiring time rather
|
|
73
|
+
* than at the first submission.
|
|
74
|
+
*/
|
|
75
|
+
export declare const ALL_FORM_SPACE_NOTIFICATION_TASK_TYPES: NotificationTaskType[];
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { type ContentTypeMimeType, type Maybe, type Milliseconds } from '@dereekb/util';
|
|
2
|
+
import { type FormSpaceFileSlot, type FormSpaceType } from './formspace.id';
|
|
3
|
+
/**
|
|
4
|
+
* @module formspace.type
|
|
5
|
+
*
|
|
6
|
+
* The {@link FormSpaceType} registry: the per-type upload restrictions, expiration policy, and submission
|
|
7
|
+
* metadata an app declares once and both the client and the server read.
|
|
8
|
+
*
|
|
9
|
+
* The type and the factory live here, in the model folder rather than in `firebase-server`, so a client-side
|
|
10
|
+
* pre-check rejects an oversized file with the SAME rule the server enforces authoritatively. Only the
|
|
11
|
+
* service INSTANCE is constructed server-side, as a NestJS provider — the same split
|
|
12
|
+
* {@link AppCalendarTypeConfigService} uses.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Who may read and remove an individual file, among the users who already reach the FormSpace itself.
|
|
16
|
+
*
|
|
17
|
+
* Narrows access; it never widens it. A caller that cannot read the space at all is refused before this is
|
|
18
|
+
* ever consulted, so `'space'` is not "public" — it is "whoever the space already lets in".
|
|
19
|
+
*
|
|
20
|
+
* - `'space'` — anyone holding the corresponding role on the space. The DEFAULT, and the only sensible
|
|
21
|
+
* answer for a single-user form, where every file was uploaded by the one person who can reach it.
|
|
22
|
+
* - `'uploader'` — only the user who uploaded that file, per {@link FormSpaceFile.ub}. For a SHARED space
|
|
23
|
+
* whose members contribute side by side rather than collaborating on one pile: a member adds their own
|
|
24
|
+
* photos and can take them back, and cannot read or delete anybody else's. The space's own `u` is NOT
|
|
25
|
+
* exempt — on a shared space `u` is whoever the space was opened for, not an administrator of its
|
|
26
|
+
* contents, and exempting them would quietly hand one member the whole album.
|
|
27
|
+
*/
|
|
28
|
+
export type FormSpaceFileAccess = 'space' | 'uploader';
|
|
29
|
+
/**
|
|
30
|
+
* Default for {@link FormSpaceFileSlotConfig.fileAccess} and {@link FormSpaceTypeConfig.fileAccess}.
|
|
31
|
+
*
|
|
32
|
+
* `'space'`, so a type declared before per-file access existed keeps behaving exactly as it did.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_FORM_SPACE_FILE_ACCESS: FormSpaceFileAccess;
|
|
35
|
+
/**
|
|
36
|
+
* Restrictions for a single named upload slot within a {@link FormSpaceTypeConfig}.
|
|
37
|
+
*
|
|
38
|
+
* A slot is a LOGICAL position, not a file: uploading into an occupied slot supersedes what was there, so
|
|
39
|
+
* "one current resume" needs no extra bookkeeping.
|
|
40
|
+
*/
|
|
41
|
+
export interface FormSpaceFileSlotConfig {
|
|
42
|
+
/**
|
|
43
|
+
* The slot this configuration applies to.
|
|
44
|
+
*/
|
|
45
|
+
readonly slot: FormSpaceFileSlot;
|
|
46
|
+
/**
|
|
47
|
+
* Human-readable name of the slot, for tooling and logs.
|
|
48
|
+
*/
|
|
49
|
+
readonly name?: Maybe<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the space may be submitted without a file in this slot. Defaults to false.
|
|
52
|
+
*/
|
|
53
|
+
readonly required?: Maybe<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Mime types accepted in this slot. Defaults to the type's {@link FormSpaceTypeConfig.allowedMimeTypes}.
|
|
56
|
+
*/
|
|
57
|
+
readonly allowedMimeTypes?: Maybe<readonly ContentTypeMimeType[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Size cap for a file in this slot. Defaults to the type's {@link FormSpaceTypeConfig.maxFileSizeBytes}.
|
|
60
|
+
*/
|
|
61
|
+
readonly maxFileSizeBytes?: Maybe<number>;
|
|
62
|
+
/**
|
|
63
|
+
* How many files this slot may hold at once. Defaults to {@link DEFAULT_FORM_SPACE_SLOT_MAX_FILES}.
|
|
64
|
+
*
|
|
65
|
+
* At 1 the slot is a POSITION: a new upload supersedes whatever was there, which is the original "one
|
|
66
|
+
* current resume" behaviour. Above 1 it is a FOLDER: uploads accumulate, and one is refused once the
|
|
67
|
+
* folder is full rather than quietly evicting the oldest file the user put there.
|
|
68
|
+
*/
|
|
69
|
+
readonly maxFiles?: Maybe<number>;
|
|
70
|
+
/**
|
|
71
|
+
* How many files this slot must hold before the space may be submitted.
|
|
72
|
+
*
|
|
73
|
+
* Defaults to 1 when {@link required} is true, and 0 otherwise, so `required` keeps meaning exactly what it
|
|
74
|
+
* meant before folders existed.
|
|
75
|
+
*/
|
|
76
|
+
readonly minFiles?: Maybe<number>;
|
|
77
|
+
/**
|
|
78
|
+
* Who may read and remove an individual file in this slot.
|
|
79
|
+
*
|
|
80
|
+
* Defaults to the type's {@link FormSpaceTypeConfig.fileAccess}, which itself defaults to
|
|
81
|
+
* {@link DEFAULT_FORM_SPACE_FILE_ACCESS}. Narrowing it per slot is what lets one shared space hold a
|
|
82
|
+
* public banner everybody sees alongside a folder of each member's own documents.
|
|
83
|
+
*/
|
|
84
|
+
readonly fileAccess?: Maybe<FormSpaceFileAccess>;
|
|
85
|
+
/**
|
|
86
|
+
* Whether a file here must pass validation before the space may be submitted. Defaults to false.
|
|
87
|
+
*
|
|
88
|
+
* Setting it does two things: an accepted upload enters the StorageFile processing pipeline instead of
|
|
89
|
+
* being stored as-is, and a file left PENDING or judged INVALID blocks submission. A type that sets this
|
|
90
|
+
* MUST register a validator for the slot server-side; the wiring asserts that at boot.
|
|
91
|
+
*/
|
|
92
|
+
readonly validationRequired?: Maybe<boolean>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Upload, expiration, and submission configuration for a single {@link FormSpaceType}.
|
|
96
|
+
*
|
|
97
|
+
* This is the contract a FormSpace of the type is held to. It is pure data and is shared by the client and
|
|
98
|
+
* the server; the server-side PROCESSING of a submission is registered separately, as a
|
|
99
|
+
* `FormSpaceSubmissionProcessorConfig` keyed by the same type string.
|
|
100
|
+
*/
|
|
101
|
+
export interface FormSpaceTypeConfig {
|
|
102
|
+
/**
|
|
103
|
+
* The type this configuration applies to.
|
|
104
|
+
*/
|
|
105
|
+
readonly formSpaceType: FormSpaceType;
|
|
106
|
+
/**
|
|
107
|
+
* Human-readable name of the type, for tooling and logs.
|
|
108
|
+
*/
|
|
109
|
+
readonly name?: Maybe<string>;
|
|
110
|
+
/**
|
|
111
|
+
* Longer description of what the type collects.
|
|
112
|
+
*/
|
|
113
|
+
readonly description?: Maybe<string>;
|
|
114
|
+
/**
|
|
115
|
+
* The upload slots this type declares.
|
|
116
|
+
*
|
|
117
|
+
* When empty, the type accepts no uploads at all — a FormSpace can be a pure JSON container.
|
|
118
|
+
*/
|
|
119
|
+
readonly slots?: Maybe<readonly FormSpaceFileSlotConfig[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Whether a slot NOT declared in {@link slots} may be uploaded into. Defaults to false.
|
|
122
|
+
*
|
|
123
|
+
* False is the safe default: an undeclared slot has no size or mime restriction of its own, so allowing
|
|
124
|
+
* one silently widens the type's contract to the global defaults.
|
|
125
|
+
*/
|
|
126
|
+
readonly allowUndeclaredSlots?: Maybe<boolean>;
|
|
127
|
+
/**
|
|
128
|
+
* Maximum number of uploads a single FormSpace of this type may ACCEPT over its whole lifetime.
|
|
129
|
+
*
|
|
130
|
+
* Counted against the space's monotonic `uc` counter, which is why superseding a slot still consumes one:
|
|
131
|
+
* the cap bounds work done, not files retained. Defaults to {@link DEFAULT_FORM_SPACE_MAX_UPLOADS}.
|
|
132
|
+
*/
|
|
133
|
+
readonly maxUploads?: Maybe<number>;
|
|
134
|
+
/**
|
|
135
|
+
* Mime types accepted by any slot that does not narrow them further.
|
|
136
|
+
* Defaults to {@link DEFAULT_FORM_SPACE_ALLOWED_MIME_TYPES}.
|
|
137
|
+
*/
|
|
138
|
+
readonly allowedMimeTypes?: Maybe<readonly ContentTypeMimeType[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Size cap for any slot that does not narrow it further.
|
|
141
|
+
* Defaults to {@link DEFAULT_FORM_SPACE_MAX_FILE_SIZE_BYTES}.
|
|
142
|
+
*/
|
|
143
|
+
readonly maxFileSizeBytes?: Maybe<number>;
|
|
144
|
+
/**
|
|
145
|
+
* Who may read and remove an individual file in any slot that does not narrow it further.
|
|
146
|
+
* Defaults to {@link DEFAULT_FORM_SPACE_FILE_ACCESS}.
|
|
147
|
+
*/
|
|
148
|
+
readonly fileAccess?: Maybe<FormSpaceFileAccess>;
|
|
149
|
+
/**
|
|
150
|
+
* How long a newly created FormSpace of this type stays editable before the expiration sweep retires it.
|
|
151
|
+
*
|
|
152
|
+
* When absent the space never expires and `eat` is never written, which is exactly what keeps it out of
|
|
153
|
+
* the sweep's inequality query.
|
|
154
|
+
*/
|
|
155
|
+
readonly expiresIn?: Maybe<Milliseconds>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Default for {@link FormSpaceTypeConfig.maxUploads}.
|
|
159
|
+
*/
|
|
160
|
+
export declare const DEFAULT_FORM_SPACE_MAX_UPLOADS = 20;
|
|
161
|
+
/**
|
|
162
|
+
* Default for {@link FormSpaceFileSlotConfig.maxFiles}.
|
|
163
|
+
*
|
|
164
|
+
* One, so a slot declared before folders existed keeps superseding rather than silently accumulating.
|
|
165
|
+
*/
|
|
166
|
+
export declare const DEFAULT_FORM_SPACE_SLOT_MAX_FILES = 1;
|
|
167
|
+
/**
|
|
168
|
+
* Default for {@link FormSpaceTypeConfig.maxFileSizeBytes}: 10 MiB.
|
|
169
|
+
*/
|
|
170
|
+
export declare const DEFAULT_FORM_SPACE_MAX_FILE_SIZE_BYTES: number;
|
|
171
|
+
/**
|
|
172
|
+
* Default for {@link FormSpaceTypeConfig.allowedMimeTypes}.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately narrow: a form attachment is a document or an image, and an open list on the DEFAULT path is
|
|
175
|
+
* how an upload endpoint becomes a general-purpose file host.
|
|
176
|
+
*/
|
|
177
|
+
export declare const DEFAULT_FORM_SPACE_ALLOWED_MIME_TYPES: readonly ContentTypeMimeType[];
|
|
178
|
+
/**
|
|
179
|
+
* Default expiration applied to {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}-shaped types that opt in: seven days.
|
|
180
|
+
*/
|
|
181
|
+
export declare const DEFAULT_FORM_SPACE_EXPIRES_IN: Milliseconds;
|
|
182
|
+
/**
|
|
183
|
+
* The {@link FormSpaceType} of {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}, used for a type the app never registered.
|
|
184
|
+
*/
|
|
185
|
+
export declare const UNKNOWN_FORM_SPACE_TYPE: FormSpaceType;
|
|
186
|
+
/**
|
|
187
|
+
* The configuration applied to a FormSpace whose type the app did not register.
|
|
188
|
+
*
|
|
189
|
+
* An unregistered type falls back rather than throwing on purpose: a scheduled sweep over every FormSpace in
|
|
190
|
+
* the app must not be taken down by one badly-typed document. CREATION is the opposite — `createFormSpace`
|
|
191
|
+
* rejects an unregistered type outright, so this fallback only ever governs a document that already exists.
|
|
192
|
+
*
|
|
193
|
+
* It declares no slots and does not allow undeclared ones, so an unregistered space accepts no uploads.
|
|
194
|
+
*/
|
|
195
|
+
export declare const DEFAULT_FORM_SPACE_TYPE_CONFIG: FormSpaceTypeConfig;
|
|
196
|
+
/**
|
|
197
|
+
* Record of {@link FormSpaceTypeConfig} keyed by {@link FormSpaceType}.
|
|
198
|
+
*/
|
|
199
|
+
export type FormSpaceTypeConfigRecord = Record<FormSpaceType, FormSpaceTypeConfig>;
|
|
200
|
+
/**
|
|
201
|
+
* Creates a {@link FormSpaceTypeConfigRecord} from an array of configs.
|
|
202
|
+
*
|
|
203
|
+
* @param configs - The configs to index.
|
|
204
|
+
* @returns A record keyed by form space type.
|
|
205
|
+
* @throws {Error} When two configs declare the same {@link FormSpaceType}.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* const record = formSpaceTypeConfigRecord([{ formSpaceType: 'demo_example' }]);
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export declare function formSpaceTypeConfigRecord(configs: FormSpaceTypeConfig[]): FormSpaceTypeConfigRecord;
|
|
213
|
+
/**
|
|
214
|
+
* Runtime service for resolving a {@link FormSpaceTypeConfig} from a {@link FormSpaceType}.
|
|
215
|
+
*
|
|
216
|
+
* Built from a {@link FormSpaceTypeConfigRecord} via {@link appFormSpaceTypeConfigService}.
|
|
217
|
+
*/
|
|
218
|
+
export declare abstract class AppFormSpaceTypeConfigService {
|
|
219
|
+
/**
|
|
220
|
+
* All registered configs for this app.
|
|
221
|
+
*/
|
|
222
|
+
abstract readonly appFormSpaceTypeConfigRecord: FormSpaceTypeConfigRecord;
|
|
223
|
+
/**
|
|
224
|
+
* Returns the config for the given type, falling back to the service's default when it is not registered.
|
|
225
|
+
*
|
|
226
|
+
* @param formSpaceType - The type to look up.
|
|
227
|
+
*/
|
|
228
|
+
abstract configForFormSpaceType(formSpaceType: FormSpaceType): FormSpaceTypeConfig;
|
|
229
|
+
/**
|
|
230
|
+
* Returns the config for the given type, or null when it is not registered.
|
|
231
|
+
*
|
|
232
|
+
* This is what `createFormSpace` gates on: a space may only be CREATED for a type the app declared.
|
|
233
|
+
*
|
|
234
|
+
* @param formSpaceType - The type to look up.
|
|
235
|
+
*/
|
|
236
|
+
abstract registeredConfigForFormSpaceType(formSpaceType: FormSpaceType): Maybe<FormSpaceTypeConfig>;
|
|
237
|
+
/**
|
|
238
|
+
* Returns every registered {@link FormSpaceType}.
|
|
239
|
+
*/
|
|
240
|
+
abstract getAllKnownFormSpaceTypes(): FormSpaceType[];
|
|
241
|
+
/**
|
|
242
|
+
* Returns every registered {@link FormSpaceTypeConfig}.
|
|
243
|
+
*/
|
|
244
|
+
abstract getAllKnownFormSpaceTypeConfigs(): FormSpaceTypeConfig[];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Reference to an {@link AppFormSpaceTypeConfigService} instance, for dependency injection.
|
|
248
|
+
*/
|
|
249
|
+
export interface AppFormSpaceTypeConfigServiceRef {
|
|
250
|
+
readonly appFormSpaceTypeConfigService: AppFormSpaceTypeConfigService;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Creates an {@link AppFormSpaceTypeConfigService} from the given record.
|
|
254
|
+
*
|
|
255
|
+
* @param appFormSpaceTypeConfigRecord - The complete form space type registry for the application.
|
|
256
|
+
* @param defaultConfig - Config used for an unregistered type. Defaults to {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}.
|
|
257
|
+
* @returns The service.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const service = appFormSpaceTypeConfigService(formSpaceTypeConfigRecord(DEMO_FORM_SPACE_TYPE_CONFIGS));
|
|
262
|
+
* const config = service.configForFormSpaceType('demo_example');
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* @__NO_SIDE_EFFECTS__
|
|
266
|
+
*/
|
|
267
|
+
export declare function appFormSpaceTypeConfigService(appFormSpaceTypeConfigRecord: FormSpaceTypeConfigRecord, defaultConfig?: FormSpaceTypeConfig): AppFormSpaceTypeConfigService;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type ContentTypeMimeType, type Maybe, type SlashPath, type SlashPathFile, type SlashPathFolder, type SlashPathTypedFileExtension, type SlashPathUntypedFile } from '@dereekb/util';
|
|
2
|
+
import { type FirebaseAuthUserId } from '../../common/auth/auth';
|
|
3
|
+
import { type StorageFilePurpose } from '../storagefile/storagefile.id';
|
|
4
|
+
import { type StorageFilePurposeUploadPolicy, type UploadedFileTypeIdentifier } from '../storagefile/storagefile.upload';
|
|
5
|
+
import { type FormSpaceFileSlot, type FormSpaceId } from './formspace.id';
|
|
6
|
+
/**
|
|
7
|
+
* @module formspace.upload
|
|
8
|
+
*
|
|
9
|
+
* Where a FormSpace's uploads land, and how a landed file is read back into `{ formSpaceId, slot }`.
|
|
10
|
+
*
|
|
11
|
+
* ONE purpose for every FormSpace file, across every type. The per-type rules live in the
|
|
12
|
+
* {@link FormSpaceTypeConfig} registry and are enforced by the initializer against the loaded FormSpace, so
|
|
13
|
+
* a new form type needs no new purpose, no new storage-rules block, and no new initializer.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* {@link UploadedFileTypeIdentifier} for a file uploaded into a FormSpace.
|
|
17
|
+
*/
|
|
18
|
+
export declare const FORM_SPACE_UPLOADED_FILE_TYPE_IDENTIFIER: UploadedFileTypeIdentifier;
|
|
19
|
+
/**
|
|
20
|
+
* The single {@link StorageFilePurpose} carried by every FormSpace upload.
|
|
21
|
+
*/
|
|
22
|
+
export declare const FORM_SPACE_PURPOSE: StorageFilePurpose;
|
|
23
|
+
/**
|
|
24
|
+
* The folder under a user's uploads folder that FormSpace uploads land in.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FORM_SPACE_UPLOADS_FOLDER_NAME = "formSpace";
|
|
27
|
+
/**
|
|
28
|
+
* Returns the uploads folder path for one FormSpace slot.
|
|
29
|
+
*
|
|
30
|
+
* @param uid - The uploading Firebase Auth user id.
|
|
31
|
+
* @param formSpaceId - The FormSpace being uploaded into.
|
|
32
|
+
* @param slot - The slot being filled.
|
|
33
|
+
* @returns The SlashPathFolder the slot's uploads land in.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* formSpaceUploadsFolderPath('user123', 'fsp1', 'resume');
|
|
38
|
+
* // 'uploads/u/user123/formSpace/fsp1/resume/'
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function formSpaceUploadsFolderPath(uid: FirebaseAuthUserId, formSpaceId: FormSpaceId, slot: FormSpaceFileSlot): SlashPathFolder;
|
|
42
|
+
/**
|
|
43
|
+
* Input for {@link formSpaceUploadsFilePath}.
|
|
44
|
+
*/
|
|
45
|
+
export interface FormSpaceUploadsFilePathInput {
|
|
46
|
+
readonly uid: FirebaseAuthUserId;
|
|
47
|
+
readonly formSpaceId: FormSpaceId;
|
|
48
|
+
readonly slot: FormSpaceFileSlot;
|
|
49
|
+
readonly filename: SlashPathFile;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns the full uploads path for one file in one FormSpace slot.
|
|
53
|
+
*
|
|
54
|
+
* The FormSpace id and the slot are IN THE PATH rather than in custom metadata because the initializer runs
|
|
55
|
+
* from a storage-triggered sweep that only ever sees the path — and because the storage rules can then keep
|
|
56
|
+
* the write inside the uploader's own namespace with no Firestore read.
|
|
57
|
+
*
|
|
58
|
+
* @param input - The uploader, the target space and slot, and the file name.
|
|
59
|
+
* @returns The full upload path.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* formSpaceUploadsFilePath({ uid: 'user123', formSpaceId: 'fsp1', slot: 'resume', filename: 'resume.pdf' });
|
|
64
|
+
* // 'uploads/u/user123/formSpace/fsp1/resume/resume.pdf'
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @__NO_SIDE_EFFECTS__
|
|
68
|
+
*/
|
|
69
|
+
export declare function formSpaceUploadsFilePath(input: FormSpaceUploadsFilePathInput): SlashPath;
|
|
70
|
+
/**
|
|
71
|
+
* Root folder every FormSpace's accepted files are moved to, out of the transient uploads folder.
|
|
72
|
+
*/
|
|
73
|
+
export declare const FORM_SPACE_FILES_ROOT_FOLDER_PATH: SlashPathFolder;
|
|
74
|
+
/**
|
|
75
|
+
* Input for {@link formSpaceFileStoragePath}.
|
|
76
|
+
*/
|
|
77
|
+
export interface FormSpaceFileStoragePathInput {
|
|
78
|
+
readonly formSpaceId: FormSpaceId;
|
|
79
|
+
readonly slot: FormSpaceFileSlot;
|
|
80
|
+
/**
|
|
81
|
+
* The index claimed from the space's `fi` counter.
|
|
82
|
+
*/
|
|
83
|
+
readonly index: number;
|
|
84
|
+
/**
|
|
85
|
+
* The extension, without its leading separator. Absent when neither the uploaded name nor its mime type
|
|
86
|
+
* named one.
|
|
87
|
+
*/
|
|
88
|
+
readonly extension?: Maybe<SlashPathTypedFileExtension>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns the permanent storage path an accepted FormSpace file is moved to.
|
|
92
|
+
*
|
|
93
|
+
* Keyed by the space, the slot, and a monotonic INDEX rather than by the uploaded name. A name-keyed
|
|
94
|
+
* destination is not unique: a file removed from the space's `f` keeps its object until the delete sweep
|
|
95
|
+
* runs, so re-uploading the same name overwrote it — leaving two StorageFiles on one object, where
|
|
96
|
+
* deleting the first destroyed the second's bytes.
|
|
97
|
+
*
|
|
98
|
+
* The leaf carries at most one separator, which is also the only shape {@link slashPathDetails} can read
|
|
99
|
+
* ({@link slashPathType} calls two or more `invalid`), so the destination always parses back into a name
|
|
100
|
+
* and an extension.
|
|
101
|
+
*
|
|
102
|
+
* @param input - The space, the slot, the claimed index, and the file's extension.
|
|
103
|
+
* @returns The permanent storage path.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* formSpaceFileStoragePath({ formSpaceId: 'fsp1', slot: 'resume', index: 0, extension: 'pdf' });
|
|
108
|
+
* // '/fsp/fsp1/resume/0.pdf'
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @__NO_SIDE_EFFECTS__
|
|
112
|
+
*/
|
|
113
|
+
export declare function formSpaceFileStoragePath(input: FormSpaceFileStoragePathInput): SlashPath;
|
|
114
|
+
/**
|
|
115
|
+
* Input for {@link formSpaceUploadFileNameDetails}.
|
|
116
|
+
*/
|
|
117
|
+
export interface FormSpaceUploadFileNameDetailsInput {
|
|
118
|
+
readonly filename: SlashPathFile;
|
|
119
|
+
/**
|
|
120
|
+
* The uploaded file's content type, used to name the extension when the filename does not.
|
|
121
|
+
*/
|
|
122
|
+
readonly mimeType?: Maybe<ContentTypeMimeType>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The uploaded name, split into the parts each layer stores.
|
|
126
|
+
*/
|
|
127
|
+
export interface FormSpaceUploadFileNameDetails {
|
|
128
|
+
/**
|
|
129
|
+
* The name without its extension, for the StorageFile's `n` — which is UNTYPED by contract (see
|
|
130
|
+
* {@link StorageFileDisplayName}) because the zip builder merges it with the path's extension.
|
|
131
|
+
*
|
|
132
|
+
* Absent for a name that is nothing but an extension, such as `.gitignore`.
|
|
133
|
+
*/
|
|
134
|
+
readonly displayName?: Maybe<SlashPathUntypedFile>;
|
|
135
|
+
/**
|
|
136
|
+
* The extension for the destination leaf.
|
|
137
|
+
*/
|
|
138
|
+
readonly extension?: Maybe<SlashPathTypedFileExtension>;
|
|
139
|
+
/**
|
|
140
|
+
* The two recomposed — what the FormSpace's `f` entry records as the file's name, and what a download
|
|
141
|
+
* of it is named.
|
|
142
|
+
*/
|
|
143
|
+
readonly fileName: SlashPathFile;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Splits an uploaded filename into the display name and extension the rest of the pipeline stores.
|
|
147
|
+
*
|
|
148
|
+
* Normalizes first: an uploaded name may carry any number of separators, and {@link slashPathDetails}
|
|
149
|
+
* reads a path with two or more as `invalid` and yields neither a name nor an extension for it. The
|
|
150
|
+
* canonical {@link replaceInvalidFilePathTypeSeparatorsInSlashPath} collapses it to at most one, so
|
|
151
|
+
* `my.report.pdf` becomes `my_report.pdf` rather than losing its extension entirely.
|
|
152
|
+
*
|
|
153
|
+
* Falls back to the mime type for a name that has no extension, which keeps the stored object
|
|
154
|
+
* self-describing and keeps `fileName` in step with what a download or a zip entry is actually called.
|
|
155
|
+
*
|
|
156
|
+
* @param input - The uploaded filename and its content type.
|
|
157
|
+
* @returns The display name, the extension, and the two recomposed.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* formSpaceUploadFileNameDetails({ filename: 'resume.pdf' });
|
|
162
|
+
* // { displayName: 'resume', extension: 'pdf', fileName: 'resume.pdf' }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @__NO_SIDE_EFFECTS__
|
|
166
|
+
*/
|
|
167
|
+
export declare function formSpaceUploadFileNameDetails(input: FormSpaceUploadFileNameDetailsInput): FormSpaceUploadFileNameDetails;
|
|
168
|
+
/**
|
|
169
|
+
* The pieces {@link parseFormSpaceUploadPath} recovers from a FormSpace upload path.
|
|
170
|
+
*/
|
|
171
|
+
export interface ParsedFormSpaceUploadPath {
|
|
172
|
+
readonly uid: FirebaseAuthUserId;
|
|
173
|
+
readonly formSpaceId: FormSpaceId;
|
|
174
|
+
readonly slot: FormSpaceFileSlot;
|
|
175
|
+
readonly filename: SlashPathFile;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Reads a FormSpace upload path back into its parts.
|
|
179
|
+
*
|
|
180
|
+
* Returns null for anything that is not exactly a FormSpace upload path, including a path with extra
|
|
181
|
+
* segments: a nested path would otherwise resolve to a slot name that no config declares, and silently
|
|
182
|
+
* widening the parse is how an upload lands somewhere nobody validated.
|
|
183
|
+
*
|
|
184
|
+
* @param path - The path to parse.
|
|
185
|
+
* @returns The parsed parts, or null when the path is not a FormSpace upload path.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* parseFormSpaceUploadPath('uploads/u/user123/formSpace/fsp1/resume/resume.pdf');
|
|
190
|
+
* // { uid: 'user123', formSpaceId: 'fsp1', slot: 'resume', filename: 'resume.pdf' }
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* @__NO_SIDE_EFFECTS__
|
|
194
|
+
*/
|
|
195
|
+
export declare function parseFormSpaceUploadPath(path: SlashPath): Maybe<ParsedFormSpaceUploadPath>;
|
|
196
|
+
/**
|
|
197
|
+
* Upload policy for {@link FORM_SPACE_PURPOSE}.
|
|
198
|
+
*
|
|
199
|
+
* The caps here are the OUTER bound — the widest a FormSpace upload may ever be — and they are what
|
|
200
|
+
* `storage.rules` mirrors. The per-type and per-slot rules in the {@link FormSpaceTypeConfig} registry
|
|
201
|
+
* narrow it further, and are enforced by the initializer, which is the only layer that can read the
|
|
202
|
+
* FormSpace to learn which type it even is.
|
|
203
|
+
*/
|
|
204
|
+
export declare const FORM_SPACE_UPLOAD_POLICY: StorageFilePurposeUploadPolicy;
|