@elaraai/e3-cloud-files 1.0.94 → 1.0.97

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 (49) hide show
  1. package/README.md +129 -23
  2. package/dist/src/authoring.d.ts +61 -24
  3. package/dist/src/authoring.d.ts.map +1 -1
  4. package/dist/src/authoring.js +92 -48
  5. package/dist/src/authoring.js.map +1 -1
  6. package/dist/src/check/index.d.ts +3 -4
  7. package/dist/src/check/index.d.ts.map +1 -1
  8. package/dist/src/check/index.js +3 -4
  9. package/dist/src/check/index.js.map +1 -1
  10. package/dist/src/check.d.ts +44 -9
  11. package/dist/src/check.d.ts.map +1 -1
  12. package/dist/src/check.js +130 -53
  13. package/dist/src/check.js.map +1 -1
  14. package/dist/src/compat.d.ts +384 -0
  15. package/dist/src/compat.d.ts.map +1 -0
  16. package/dist/src/compat.js +258 -0
  17. package/dist/src/compat.js.map +1 -0
  18. package/dist/src/index.d.ts +12 -9
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +12 -9
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/s3/index.d.ts.map +1 -1
  23. package/dist/src/s3/index.js +34 -12
  24. package/dist/src/s3/index.js.map +1 -1
  25. package/dist/src/storage/index.d.ts +72 -3
  26. package/dist/src/storage/index.d.ts.map +1 -1
  27. package/dist/src/storage/index.js +258 -13
  28. package/dist/src/storage/index.js.map +1 -1
  29. package/dist/src/sync/engine.d.ts +282 -49
  30. package/dist/src/sync/engine.d.ts.map +1 -1
  31. package/dist/src/sync/engine.js +599 -110
  32. package/dist/src/sync/engine.js.map +1 -1
  33. package/dist/src/sync/index.d.ts +3 -2
  34. package/dist/src/sync/index.d.ts.map +1 -1
  35. package/dist/src/sync/index.js +3 -2
  36. package/dist/src/sync/index.js.map +1 -1
  37. package/dist/src/sync/memory.d.ts +67 -12
  38. package/dist/src/sync/memory.d.ts.map +1 -1
  39. package/dist/src/sync/memory.js +248 -18
  40. package/dist/src/sync/memory.js.map +1 -1
  41. package/dist/src/sync/push.d.ts +56 -0
  42. package/dist/src/sync/push.d.ts.map +1 -0
  43. package/dist/src/sync/push.js +205 -0
  44. package/dist/src/sync/push.js.map +1 -0
  45. package/dist/test/typed.examples.d.ts +49 -0
  46. package/dist/test/typed.examples.d.ts.map +1 -0
  47. package/dist/test/typed.examples.js +58 -0
  48. package/dist/test/typed.examples.js.map +1 -0
  49. package/package.json +10 -6
package/README.md CHANGED
@@ -9,8 +9,17 @@ changed, the same way on a local repository and on a deployed workspace.
9
9
  - **Your data stays in the bucket.** Locally, a sync reads the bucket with
10
10
  your own cloud credentials. The bytes land only in the local e3 repository,
11
11
  never in your project.
12
- - **A file arrives as it is**, a `Blob`. A task decodes it in East: as East's
13
- JSON, as CSV, or as text.
12
+ - **A file arrives as it is**, a `Blob`, and a task decodes it in East: as
13
+ East's JSON, as CSV, or as text. Or, named with a **type**, a beast2 file
14
+ arrives as the value it holds, an input of that type with no task in
15
+ between, a collection of any size.
16
+ - **Records are seeded and fed from files.** A record's mutation may be fed
17
+ by files: each new file is the mutation's one argument, applied once.
18
+ (A record seeded from a file, `upload.record`, lands with the next e3
19
+ release: see below.)
20
+ - **Push from disk.** `e3-cloud files push` puts files from your machine
21
+ into a source and takes them in, with your e3 login alone where the source
22
+ is the deployment's own bucket.
14
23
  - **Nothing polls.** A sync runs when someone asks for it, and reads only the
15
24
  files that changed since the last one.
16
25
 
@@ -54,9 +63,12 @@ export default Files.manifest([upload]);
54
63
  | Shape | Selects | The input's type | Largest file |
55
64
  |---|---|---|---|
56
65
  | `file(input, { key })` | One key | `Blob` | 1 GiB less 64 KiB |
66
+ | `file(input, { key, type })` | One key, a beast2 file of `type` | `type` | 1 GiB for a value that is no collection; a collection any size (a segment at most 64 MiB) |
57
67
  | `latest(input, { prefix, include? })` | The greatest key under the prefix that matches | `Blob` | 1 GiB less 64 KiB |
68
+ | `latest(input, { prefix, include?, type })` | The greatest key, a beast2 file of `type` | `type` | as a typed `file` |
58
69
  | `folder(input, { prefix, include? })` | Every key under the prefix that matches, by its path under the prefix | `Dict<String, Blob>` | 64 MiB less 64 KiB each |
59
70
  | `listing(input, { prefix, include? })` | The same keys' size, time and ETag, without their bytes | `Dict<String, Struct{size, modified, etag}>` | none |
71
+ | `mutation(def, { key } \| { prefix, include? })` | One key, or every file under a prefix its glob matches, each new one in key order: a beast2 file of the mutation's one argument's type | (the mutation's, as declared) | 1 GiB |
60
72
 
61
73
  - **Each shape returns an ordinary `e3.input`.** It has no default, so it
62
74
  stays unassigned until a sync sets it. A task takes it like any input, and
@@ -72,8 +84,60 @@ export default Files.manifest([upload]);
72
84
  subfolders' too, their paths naming the subfolder (`2026/10/a.json`).
73
85
  - **Mistakes are refused as they are declared** (`FilesDeclarationError`, one
74
86
  line per problem): a name e3 refuses, a key that isn't relative, a glob
75
- that can't match, and in `Files.manifest` two sources of one name or two
76
- shapes filling one input.
87
+ that can't match, a `patch` mutation or one of another arity than one, and
88
+ in `Files.manifest` two sources of one name or two shapes filling one
89
+ target.
90
+
91
+ ## Typed files and fed records
92
+
93
+ A beast2 file already is an East value: the East SDK (`encodeBeast2For`,
94
+ `Beast2ElementWriter`), east-py, `e3 convert` and `e3 dataset get -f beast2`
95
+ all write it. A `file` or `latest` that names a `type` is an input of that
96
+ type, and the sync takes the file in as the value it holds, with no task in
97
+ between:
98
+
99
+ ```ts
100
+ // src/package.ts
101
+ export const Price = StructType({ sku: StringType, price: FloatType });
102
+ export const prices = upload.file('prices', { key: 'prices.beast2', type: ArrayType(Price) }); // an input of Array<Price>
103
+ export const stock = upload.latest('stock', { prefix: 'stock/', type: ArrayType(Stock) }); // the greatest key, typed
104
+
105
+ export const priceBook = e3.record('price_book', DictType(StringType, FloatType), new Map());
106
+ export const loadPrices = e3.mutation.reduce('load_prices', priceBook, East.function([PriceBook, ArrayType(Price)], PriceBook, …));
107
+ export const summary = e3.task('price_summary', [prices, priceBook], …); // used as any dataset
108
+
109
+ // src/files.ts
110
+ upload.mutation(loadPrices, { prefix: 'deltas/', include: '*.beast2' }); // each new delta file calls load_prices with its value
111
+ export default Files.manifest([upload]);
112
+ ```
113
+
114
+ - **The header first.** The sync reads a typed file's beast2 header before
115
+ any other byte of it and refuses a file of another type (`type_mismatch`,
116
+ naming both types) or no beast2 (`not_beast2`); the input keeps its value
117
+ and the sync goes on. A deploy of the declarations checks the type against
118
+ the package's input, record or mutation too.
119
+ - **Any size.** A collection (an `Array`, a `Set`, a `Dict`) is taken in by
120
+ pieces, as e3 takes in any delivery, however large; in the cloud never
121
+ through a disk. A value that is no collection is held whole: at most 1 GiB.
122
+ A file whose segments are larger than beast2's 64 MiB is refused naming
123
+ the fix (write it again with a current writer).
124
+ - **The same bytes again change nothing.** A file re-uploaded unchanged (a
125
+ new ETag, the same SHA-256) gives the same value: nothing downstream runs.
126
+ - **A fed mutation** (`upload.mutation(def, …)`) is the package's own, a
127
+ `reduce` or an `edit` taking exactly one argument (a `patch` cannot be
128
+ fed). Every new file the mapping selects is applied, in key order, once by
129
+ its bytes' hash: a sync run again applies nothing twice, and the same bytes
130
+ under another name are applied no more; a file the mutation refuses
131
+ (`invalid`, `failed`, `timed_out`) is reported and tried again only once it
132
+ changes, and the files after it wait; a feed of a record with no state yet,
133
+ or one busy, waits for the next sync. Its commit reads in the record's
134
+ history as any mutation's, its argument the delivery.
135
+ - **A seeded record** (`upload.record(name, type, { key })`, a record pending
136
+ until its file arrives, which is its first state) needs the e3 release
137
+ that lets a record be declared with no initial value
138
+ (elaraai/east-workspace#1363): the declarations, the checks (`record`
139
+ targets, `$init` and `$seed` commits, `--reset`) and the sync's seeds are
140
+ in place, and the declaration itself lands with that release.
77
141
 
78
142
  ## Decoding in a task
79
143
 
@@ -138,13 +202,38 @@ e3 dataflow run ./repo dev
138
202
  only each folder's n greatest keys, which caps what comes down to your
139
203
  machine.
140
204
  - **`files sync`** lists each source and compares every file with what its
141
- input took last, by ETag and size. It reads only what is new or changed,
142
- writes only the inputs whose files changed, and says what it did per input.
143
- `--input` syncs only those inputs; `--dry-run` lists what a sync would take
144
- and takes nothing. A sync doesn't run the dataflow: `e3 dataflow run` does, and the
145
- sync prints the command when anything changed.
146
- - **`files status`** shows the declarations, where each source is bound, and
147
- the last sync.
205
+ target took last, by ETag and size. It reads only what is new or changed,
206
+ writes only the targets whose files changed, and says what it did per
207
+ target: the rows taken in, the commit a feed made, what was refused and
208
+ why. `--target` syncs only those (an input's name, `record:<name>`,
209
+ `mutation:<record>.<name>`, split at its last dot, so a mutation whose
210
+ name holds a dot is named only through the API's structured targets);
211
+ `--reset <record>` takes a seeded record's
212
+ file as its state again whatever has mutated it since; `--dry-run` lists
213
+ what a sync would take and takes nothing. A sync doesn't run the dataflow:
214
+ `e3 dataflow run` does, and the sync prints the command when anything
215
+ changed.
216
+ - **`files push`** puts files from disk into a source and takes them in:
217
+ each file is matched to the mapping that takes it (a `file` by its name, a
218
+ `latest` under its prefix, a directory to the `folder`; or `--as <target>`
219
+ names one for every file), checked against the mapping's type and limit,
220
+ put where the source is bound (with your e3 login alone where that is the
221
+ deployment's own bucket: the platform signs the upload, bound to the
222
+ file's SHA-256; with your own credentials, `--profile`, elsewhere), then
223
+ one sync of what was put. A file the source holds already is `unchanged`;
224
+ `--dry-run` plans and puts nothing. Running it again puts only what is
225
+ not there.
226
+ - **`files status`** shows each mapping with its shape, its type, the file
227
+ it holds and the commit it made (or `pending` for a record not yet
228
+ seeded), where each source is bound, and the last sync.
229
+
230
+ ```
231
+ $ e3-cloud files push https://e3.example.com/repos/acme dev ./data/prices.beast2 ./data/deltas/2026-10-19.beast2
232
+ upload s3://acme-files/acme/dev/
233
+ prices.beast2 → prices Array<Struct{sku, price}> 38 MiB put taken in 412,310 rows
234
+ 2026-10-19.beast2 → mutation:price_book.load_prices load_prices 1 MiB put committed 9f2c1a0b7e3d
235
+ Next: e3 dataflow run https://e3.example.com/repos/acme dev
236
+ ```
148
237
 
149
238
  A sync is safe to repeat and to stop. Each input is remembered only once it
150
239
  is written, so a sync that stops part way takes the rest next time.
@@ -229,8 +318,11 @@ is read. So a sync holds one batch of files and the pieces it changes,
229
318
  however large the folder; the first sync of 6 GB of files holds 256 MiB at a
230
319
  time, and a sync stopped part way takes the rest next time.
231
320
 
232
- A `file` or `latest` input is held whole while it is written: a 1 GiB file
233
- needs some 3 GiB of memory.
321
+ A `file` or `latest` input with no type is held whole while it is written: a
322
+ 1 GiB file needs some 3 GiB of memory. A typed collection is never held
323
+ whole: in the cloud it is staged in the data bucket as it streams and taken
324
+ in by intake units on the sandbox functions; locally it is streamed to the
325
+ repository's own scratch and taken in by e3's door for a file.
234
326
 
235
327
  ### On a local repository
236
328
 
@@ -248,8 +340,11 @@ and profile it was: `aws sso login --profile <name>` renews it.
248
340
 
249
341
  `@elaraai/e3-cloud-files/check` holds the checks without the authoring:
250
342
 
251
- - `checkFiles(manifest, inputs)`: the declarations against a package's inputs
252
- (`readPackageInputs(read, packageHash)` reads them from its objects);
343
+ - `checkFiles(manifest, surface)`: the declarations against a package's
344
+ inputs, records and mutations (`readPackageSurface(read, packageHash)`
345
+ reads them from its objects): an input writable and of the mapping's type,
346
+ a record of the file's type, a mutation of the record taking one argument
347
+ of the file's type;
253
348
  - `checkBindings(manifest, bindings, policy)`: bindings against the
254
349
  declarations, and in the cloud against the sources the deployment grants.
255
350
 
@@ -260,13 +355,23 @@ rendered as one line by `filesProblemText`.
260
355
 
261
356
  In `@elaraai/e3-cloud-types`, re-exported here:
262
357
 
263
- - `FilesManifestType`: the sources' names, and each mapping's input, source
264
- and shape (`FilesShapeType`: `file`, `latest`, `folder`, `listing`).
358
+ - `FilesManifestType`: the sources' names, and each mapping's target
359
+ (`FilesTargetType`: `input`, `record` or `mutation`), source and shape
360
+ (`FilesShapeType`: `file` and `latest`, each with an optional `type`;
361
+ `folder`, `listing`).
265
362
  - `FilesBindingType`: a source's URL, region, and, locally, profile and
266
363
  `latest`.
267
- - `FilesSyncReportType`: per input, the files new, changed, removed,
268
- unchanged and refused, the bytes taken in, and whether it was written.
269
- - `FilesStatusType`, `FilesProblemType`.
364
+ - `FilesSyncReportType`: per target, the files new, changed, removed,
365
+ unchanged and refused (`FilesRefusalReasonType`: `too_large`,
366
+ `segment_too_large`, `unreadable`, `not_beast2`, `type_mismatch`,
367
+ `mutation`, `missing`), the bytes taken in, whether it was written, the
368
+ typed value taken (`FilesTakenType`: its rows), the commit a seed or feed
369
+ made, and why a seed was kept or a feed waits.
370
+ - `FilesStatusType` (each mapping with what it holds and whether its record
371
+ is pending), `FilesProblemType`, the push's `FilesPushRequestType` and
372
+ `FilesPushPlanType`.
373
+ - Values stored by the first version (mappings by input) are read through
374
+ `@elaraai/e3-cloud-files/compat`, which lifts them.
270
375
  - The inputs' types: `FilesFileType` (`Blob`), `FilesFolderType`
271
376
  (`Dict<String, Blob>`), `FilesListingType`.
272
377
 
@@ -276,10 +381,11 @@ In `@elaraai/e3-cloud-types`, re-exported here:
276
381
  |---|---|
277
382
  | `@elaraai/e3-cloud-files` | Authoring: `Files.source`, the shapes, `Files.manifest`, and the types |
278
383
  | `@elaraai/e3-cloud-files/check` | The checks, without the authoring |
279
- | `@elaraai/e3-cloud-files/sync` | The sync (`syncFiles`) over what reads, writes and remembers, and those three in memory for tests |
280
- | `@elaraai/e3-cloud-files/storage` | The sync's writer over an e3 store (needs `@elaraai/e3-core`) |
384
+ | `@elaraai/e3-cloud-files/sync` | The sync (`syncFiles`) over what reads, writes and remembers, the push's plan (`planPush`), and the ports in memory for tests |
385
+ | `@elaraai/e3-cloud-files/storage` | The sync's writer over an e3 store, and the cloud's over a staging (needs `@elaraai/e3-core`) |
281
386
  | `@elaraai/e3-cloud-files/s3` | The sync's reader of a bucket (needs `@aws-sdk/client-s3`) |
282
- | `@elaraai/e3-cloud-files/examples/*` | The worked example (`orders`) and the decoding examples (`decode`) |
387
+ | `@elaraai/e3-cloud-files/compat` | The first version's stored types, and their lift into the current ones |
388
+ | `@elaraai/e3-cloud-files/examples/*` | The worked examples (`orders`, `typed`) and the decoding examples (`decode`) |
283
389
 
284
390
  ## License
285
391
 
@@ -3,29 +3,42 @@
3
3
  * Licensed under BSL 1.1. See LICENSE for details.
4
4
  */
5
5
  /**
6
- * Declaring which inputs come from files.
6
+ * Declaring which inputs come from files, and which records are seeded or
7
+ * fed from them.
7
8
  *
8
9
  * ```ts
9
10
  * export const upload = Files.source('upload');
10
- * export const rates = upload.file('rates', { key: 'rates.json' });
11
- * export const stock = upload.latest('stock', { prefix: 'stock/' });
12
- * export const orders = upload.folder('orders', { prefix: 'orders/', include: '*.json' });
11
+ * export const rates = upload.file('rates', { key: 'rates.json' }); // a Blob
12
+ * export const orders = upload.file('orders', { key: 'orders.beast2', type: ArrayType(Order) }); // an Array<Order>
13
+ * export const stock = upload.latest('stock', { prefix: 'stock/', type: ArrayType(Stock) });
14
+ * export const files = upload.folder('invoices', { prefix: 'invoices/', include: '*.pdf' });
15
+ * upload.mutation(loadPrices, { key: 'prices.beast2' }); // each new file calls loadPrices
13
16
  * export default Files.manifest([upload]);
14
17
  * ```
15
18
  *
16
- * Each declaration is an ordinary `e3.input` with no default, so the package
17
- * is plain e3 and tasks take these inputs as any other. A sync fills them;
18
- * until the first, they are unassigned. What each source's files fill is the
19
- * {@link Files.manifest}, which `e3-cloud files deploy` deploys beside the
20
- * package.
19
+ * Each input declaration is an ordinary `e3.input` with no default, so the
20
+ * package is plain e3 and tasks take these inputs as any other. A sync fills
21
+ * them; until the first, they are unassigned. A `file` or `latest` that
22
+ * names a `type` is an input of that type, filled with the beast2 value the
23
+ * file holds; one that names none is a `Blob`, filled with the file's
24
+ * bytes. A mutation declared here is the package's own (`e3.mutation.reduce`
25
+ * or `edit`, taking one argument): a sync calls it once with each new file's
26
+ * value. What each source's files fill is the {@link Files.manifest}, which
27
+ * `e3-cloud files deploy` deploys beside the package.
21
28
  *
22
29
  * @packageDocumentation
23
30
  */
24
- import e3 from '@elaraai/e3';
31
+ import e3, { type MutationDef } from '@elaraai/e3';
25
32
  import { type EastType } from '@elaraai/east';
26
33
  import { FilesFileType, FilesFolderType, FilesListingType, type FilesManifest, type FilesMapping, type FilesProblem } from '@elaraai/e3-cloud-types';
27
- /** The input a declaration makes: an `e3.input` of its shape's type, with no default. */
34
+ /** The input a declaration makes: an `e3.input` of its type, with no default. */
28
35
  export type FilesInput<Name extends string, T extends EastType> = ReturnType<typeof e3.input<Name, T>>;
36
+ /** The mutation a feed declaration hands back: the package's own definition, as it was given. */
37
+ export type FilesMutation<M extends MutationDef> = M;
38
+ /** One file, by its key relative to where the workspace binds the source. */
39
+ export interface FilesKeySpec {
40
+ readonly key: string;
41
+ }
29
42
  /** The selection of `latest`, `folder` and `listing`: a prefix, and a glob over the paths under it. */
30
43
  export interface FilesSelection {
31
44
  /** The folder, relative to where the workspace binds the source: `''` for all of it, else ending with `/` */
@@ -33,6 +46,14 @@ export interface FilesSelection {
33
46
  /** A glob over paths relative to the prefix: `*` within a segment, `**` across segments, `?` one character, `[a-z]` one of a set, `{csv,json}` any of its alternatives */
34
47
  readonly include?: string;
35
48
  }
49
+ /** One file, typed: the beast2 value of `type` it holds. */
50
+ export interface FilesTypedKeySpec<T extends EastType> extends FilesKeySpec {
51
+ readonly type: T;
52
+ }
53
+ /** The newest file of a selection, typed. */
54
+ export interface FilesTypedSelection<T extends EastType> extends FilesSelection {
55
+ readonly type: T;
56
+ }
36
57
  /** Thrown when declarations are not well-formed, naming every problem. */
37
58
  export declare class FilesDeclarationError extends Error {
38
59
  /** Every problem */
@@ -41,7 +62,7 @@ export declare class FilesDeclarationError extends Error {
41
62
  }
42
63
  /**
43
64
  * A source of files: a name each workspace binds to a bucket and a prefix,
44
- * and the inputs its files fill.
65
+ * and the inputs, records and mutations its files fill.
45
66
  */
46
67
  export declare class FilesSource {
47
68
  #private;
@@ -49,27 +70,30 @@ export declare class FilesSource {
49
70
  readonly name: string;
50
71
  /** @internal Use {@link Files.source}. */
51
72
  constructor(name: string);
52
- /** The inputs this source fills, in the order they were declared. */
73
+ /** The targets this source fills, in the order they were declared. */
53
74
  get mappings(): readonly FilesMapping[];
54
75
  /**
55
- * One file, as a `Blob`: the input `.inputs.<input>`.
76
+ * One file: the input `.inputs.<input>`. As a `Blob`, its bytes as they
77
+ * are; or, with `type`, as the beast2 value of that type the file holds
78
+ * (a collection of any size, taken in by pieces).
56
79
  *
57
80
  * @param input - The input's name
58
- * @param spec - `key`: the file, relative to where the workspace binds the source
81
+ * @param spec - `key`: the file, relative to where the workspace binds the source; `type`: the value it holds
59
82
  * @returns The input, for tasks to take
60
83
  */
61
- file<Name extends string>(input: Name, spec: {
62
- readonly key: string;
63
- }): FilesInput<Name, typeof FilesFileType>;
84
+ file<Name extends string>(input: Name, spec: FilesKeySpec): FilesInput<Name, typeof FilesFileType>;
85
+ file<Name extends string, T extends EastType>(input: Name, spec: FilesTypedKeySpec<T>): FilesInput<Name, T>;
64
86
  /**
65
- * The newest file under a prefix, as a `Blob`: the greatest key that matches
66
- * `include`, so files named by their time pick the latest.
87
+ * The newest file under a prefix: the greatest key that matches
88
+ * `include`, so files named by their time pick the latest. As a `Blob`,
89
+ * or, with `type`, as the value it holds.
67
90
  *
68
91
  * @param input - The input's name
69
- * @param spec - The prefix, and a glob over the paths under it
92
+ * @param spec - The prefix, a glob over the paths under it, and the value's type
70
93
  * @returns The input, for tasks to take
71
94
  */
72
95
  latest<Name extends string>(input: Name, spec: FilesSelection): FilesInput<Name, typeof FilesFileType>;
96
+ latest<Name extends string, T extends EastType>(input: Name, spec: FilesTypedSelection<T>): FilesInput<Name, T>;
73
97
  /**
74
98
  * Every file under a prefix that matches `include`, as a
75
99
  * `Dict<String, Blob>` keyed by each file's path relative to the prefix.
@@ -89,6 +113,19 @@ export declare class FilesSource {
89
113
  * @returns The input, for tasks to take
90
114
  */
91
115
  listing<Name extends string>(input: Name, spec: FilesSelection): FilesInput<Name, typeof FilesListingType>;
116
+ /**
117
+ * A record's mutation, fed: a sync calls it once with each new file's
118
+ * value, which must be of the mutation's one argument's type. The
119
+ * mutation is the package's own, a `reduce` or an `edit` taking exactly
120
+ * one argument; a `patch` mutation takes a patch, not a value, so it
121
+ * cannot be fed.
122
+ *
123
+ * @param def - The mutation: `e3.mutation.reduce(...)` or `e3.mutation.edit(...)`
124
+ * @param spec - One file (`key`), or the newest under a prefix (`prefix`, `include`)
125
+ * @returns The mutation, as given
126
+ * @throws {FilesDeclarationError} When the mutation takes other than one argument, or is a `patch`
127
+ */
128
+ mutation<M extends MutationDef>(def: M, spec: FilesKeySpec | FilesSelection): FilesMutation<M>;
92
129
  }
93
130
  /** Declaring files. */
94
131
  export declare const Files: {
@@ -96,18 +133,18 @@ export declare const Files: {
96
133
  * A source of files, which each workspace binds to a bucket and a prefix.
97
134
  *
98
135
  * @param name - Its name, unique among a solution's sources
99
- * @returns The source, whose methods declare the inputs it fills
136
+ * @returns The source, whose methods declare what its files fill
100
137
  * @throws {FilesDeclarationError} When the name is one e3 refuses
101
138
  */
102
139
  source(name: string): FilesSource;
103
140
  /**
104
141
  * The declarations `e3-cloud files deploy` deploys: the sources, and the
105
- * inputs their files fill.
142
+ * inputs, records and mutations their files fill.
106
143
  *
107
144
  * @param sources - Every source
108
145
  * @returns The manifest, an East value of `FilesManifestType`
109
146
  * @throws {FilesDeclarationError} When two sources share a name, or two
110
- * mappings fill one input
147
+ * mappings fill one target
111
148
  */
112
149
  manifest(sources: readonly FilesSource[]): FilesManifest;
113
150
  };
@@ -1 +1 @@
1
- {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../src/authoring.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EACL,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAEhD,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EACzD,MAAM,yBAAyB,CAAC;AAGjC,yFAAyF;AACzF,MAAM,MAAM,UAAU,CAAC,IAAI,SAAS,MAAM,EAAE,CAAC,SAAS,QAAQ,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAEvG,uGAAuG;AACvG,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0KAA0K;IAC1K,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,0EAA0E;AAC1E,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,oBAAoB;IACpB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;gBAE/B,QAAQ,EAAE,SAAS,YAAY,EAAE;CAK9C;AA8BD;;;GAGG;AACH,qBAAa,WAAW;;IACtB,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,0CAA0C;gBAC9B,IAAI,EAAE,MAAM;IAKxB,qEAAqE;IACrE,IAAI,QAAQ,IAAI,SAAS,YAAY,EAAE,CAEtC;IASD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,aAAa,CAAC;IAM9G;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,aAAa,CAAC;IAItG;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,eAAe,CAAC;IAIxG;;;;;;;;OAQG;IACH,OAAO,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,gBAAgB,CAAC;CAG3G;AAED,uBAAuB;AACvB,eAAO,MAAM,KAAK;IAChB;;;;;;OAMG;iBACU,MAAM,GAAG,WAAW;IAIjC;;;;;;;;OAQG;sBACe,SAAS,WAAW,EAAE,GAAG,aAAa;CAmBzD,CAAC"}
1
+ {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../src/authoring.ts"],"names":[],"mappings":"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"}
@@ -3,28 +3,35 @@
3
3
  * Licensed under BSL 1.1. See LICENSE for details.
4
4
  */
5
5
  /**
6
- * Declaring which inputs come from files.
6
+ * Declaring which inputs come from files, and which records are seeded or
7
+ * fed from them.
7
8
  *
8
9
  * ```ts
9
10
  * export const upload = Files.source('upload');
10
- * export const rates = upload.file('rates', { key: 'rates.json' });
11
- * export const stock = upload.latest('stock', { prefix: 'stock/' });
12
- * export const orders = upload.folder('orders', { prefix: 'orders/', include: '*.json' });
11
+ * export const rates = upload.file('rates', { key: 'rates.json' }); // a Blob
12
+ * export const orders = upload.file('orders', { key: 'orders.beast2', type: ArrayType(Order) }); // an Array<Order>
13
+ * export const stock = upload.latest('stock', { prefix: 'stock/', type: ArrayType(Stock) });
14
+ * export const files = upload.folder('invoices', { prefix: 'invoices/', include: '*.pdf' });
15
+ * upload.mutation(loadPrices, { key: 'prices.beast2' }); // each new file calls loadPrices
13
16
  * export default Files.manifest([upload]);
14
17
  * ```
15
18
  *
16
- * Each declaration is an ordinary `e3.input` with no default, so the package
17
- * is plain e3 and tasks take these inputs as any other. A sync fills them;
18
- * until the first, they are unassigned. What each source's files fill is the
19
- * {@link Files.manifest}, which `e3-cloud files deploy` deploys beside the
20
- * package.
19
+ * Each input declaration is an ordinary `e3.input` with no default, so the
20
+ * package is plain e3 and tasks take these inputs as any other. A sync fills
21
+ * them; until the first, they are unassigned. A `file` or `latest` that
22
+ * names a `type` is an input of that type, filled with the beast2 value the
23
+ * file holds; one that names none is a `Blob`, filled with the file's
24
+ * bytes. A mutation declared here is the package's own (`e3.mutation.reduce`
25
+ * or `edit`, taking one argument): a sync calls it once with each new file's
26
+ * value. What each source's files fill is the {@link Files.manifest}, which
27
+ * `e3-cloud files deploy` deploys beside the package.
21
28
  *
22
29
  * @packageDocumentation
23
30
  */
24
31
  import e3 from '@elaraai/e3';
25
- import { none, some, variant } from '@elaraai/east';
32
+ import { none, some, toEastTypeValue, variant } from '@elaraai/east';
26
33
  import { nameProblem } from '@elaraai/e3-types';
27
- import { FilesFileType, FilesFolderType, FilesListingType, filesProblemText, } from '@elaraai/e3-cloud-types';
34
+ import { FilesFileType, FilesFolderType, FilesListingType, filesProblemText, sameFilesTarget, } from '@elaraai/e3-cloud-types';
28
35
  import { includeProblem, keyProblem, prefixProblem } from './paths.js';
29
36
  /** Thrown when declarations are not well-formed, naming every problem. */
30
37
  export class FilesDeclarationError extends Error {
@@ -46,16 +53,21 @@ function nameProblems(kind, name) {
46
53
  const reason = nameProblem('workspace', name);
47
54
  return reason === null ? [] : [variant('invalid_name', { kind, name, reason })];
48
55
  }
56
+ /** A key's problems. */
57
+ function keyProblems(target, key) {
58
+ const reason = keyProblem(key);
59
+ return reason === null ? [] : [variant('invalid_key', { target, key, reason })];
60
+ }
49
61
  /** A selection's problems. */
50
- function selectionProblems(input, selection) {
62
+ function selectionProblems(target, selection) {
51
63
  const problems = [];
52
64
  const prefix = prefixProblem(selection.prefix);
53
65
  if (prefix !== null)
54
- problems.push(variant('invalid_key', { input, key: selection.prefix, reason: prefix }));
66
+ problems.push(variant('invalid_key', { target, key: selection.prefix, reason: prefix }));
55
67
  if (selection.include !== undefined) {
56
68
  const include = includeProblem(selection.include);
57
69
  if (include !== null)
58
- problems.push(variant('invalid_include', { input, include: selection.include, reason: include }));
70
+ problems.push(variant('invalid_include', { target, include: selection.include, reason: include }));
59
71
  }
60
72
  return problems;
61
73
  }
@@ -63,9 +75,23 @@ function selectionProblems(input, selection) {
63
75
  function selected(selection) {
64
76
  return { prefix: selection.prefix, include: selection.include === undefined ? none : some(selection.include) };
65
77
  }
78
+ /** Whether a spec names one file, rather than a selection. */
79
+ function isKeySpec(spec) {
80
+ return 'key' in spec;
81
+ }
82
+ /**
83
+ * The shape of a spec that selects one file, typed: `file` or `latest`, with
84
+ * the type, and its problems.
85
+ */
86
+ function oneFileShape(target, spec, type) {
87
+ const typeValue = some(toEastTypeValue(type));
88
+ if (isKeySpec(spec))
89
+ return { shape: variant('file', { key: spec.key, type: typeValue }), problems: keyProblems(target, spec.key) };
90
+ return { shape: variant('latest', { ...selected(spec), type: typeValue }), problems: selectionProblems(target, spec) };
91
+ }
66
92
  /**
67
93
  * A source of files: a name each workspace binds to a bucket and a prefix,
68
- * and the inputs its files fill.
94
+ * and the inputs, records and mutations its files fill.
69
95
  */
70
96
  export class FilesSource {
71
97
  /** The source's name */
@@ -76,38 +102,31 @@ export class FilesSource {
76
102
  refuse(nameProblems('source', name));
77
103
  this.name = name;
78
104
  }
79
- /** The inputs this source fills, in the order they were declared. */
105
+ /** The targets this source fills, in the order they were declared. */
80
106
  get mappings() {
81
107
  return this.#mappings;
82
108
  }
83
- /** Records a mapping and makes its input. */
84
- #declare(input, type, shape, problems) {
85
- refuse([...nameProblems('input', input), ...problems]);
86
- this.#mappings.push({ input, source: this.name, shape });
109
+ /** Records a mapping, refusing its problems first. */
110
+ #declare(target, shape, problems) {
111
+ refuse(problems);
112
+ this.#mappings.push({ target, source: this.name, shape });
113
+ }
114
+ /** Records an input's mapping and makes its input. */
115
+ #input(input, type, shape, problems) {
116
+ this.#declare(variant('input', input), shape, [...nameProblems('input', input), ...problems]);
87
117
  return e3.input(input, type);
88
118
  }
89
- /**
90
- * One file, as a `Blob`: the input `.inputs.<input>`.
91
- *
92
- * @param input - The input's name
93
- * @param spec - `key`: the file, relative to where the workspace binds the source
94
- * @returns The input, for tasks to take
95
- */
96
119
  file(input, spec) {
97
- const reason = keyProblem(spec.key);
98
- const problems = reason === null ? [] : [variant('invalid_key', { input, key: spec.key, reason })];
99
- return this.#declare(input, FilesFileType, variant('file', { key: spec.key }), problems);
120
+ const target = variant('input', input);
121
+ const type = 'type' in spec ? spec.type : FilesFileType;
122
+ const shape = variant('file', { key: spec.key, type: 'type' in spec ? some(toEastTypeValue(spec.type)) : none });
123
+ return this.#input(input, type, shape, keyProblems(target, spec.key));
100
124
  }
101
- /**
102
- * The newest file under a prefix, as a `Blob`: the greatest key that matches
103
- * `include`, so files named by their time pick the latest.
104
- *
105
- * @param input - The input's name
106
- * @param spec - The prefix, and a glob over the paths under it
107
- * @returns The input, for tasks to take
108
- */
109
125
  latest(input, spec) {
110
- return this.#declare(input, FilesFileType, variant('latest', selected(spec)), selectionProblems(input, spec));
126
+ const target = variant('input', input);
127
+ const type = 'type' in spec ? spec.type : FilesFileType;
128
+ const shape = variant('latest', { ...selected(spec), type: 'type' in spec ? some(toEastTypeValue(spec.type)) : none });
129
+ return this.#input(input, type, shape, selectionProblems(target, spec));
111
130
  }
112
131
  /**
113
132
  * Every file under a prefix that matches `include`, as a
@@ -118,7 +137,7 @@ export class FilesSource {
118
137
  * @returns The input, for tasks to take
119
138
  */
120
139
  folder(input, spec) {
121
- return this.#declare(input, FilesFolderType, variant('folder', selected(spec)), selectionProblems(input, spec));
140
+ return this.#input(input, FilesFolderType, variant('folder', selected(spec)), selectionProblems(variant('input', input), spec));
122
141
  }
123
142
  /**
124
143
  * The files under a prefix that match `include`, without their bytes: each
@@ -130,7 +149,32 @@ export class FilesSource {
130
149
  * @returns The input, for tasks to take
131
150
  */
132
151
  listing(input, spec) {
133
- return this.#declare(input, FilesListingType, variant('listing', selected(spec)), selectionProblems(input, spec));
152
+ return this.#input(input, FilesListingType, variant('listing', selected(spec)), selectionProblems(variant('input', input), spec));
153
+ }
154
+ /**
155
+ * A record's mutation, fed: a sync calls it once with each new file's
156
+ * value, which must be of the mutation's one argument's type. The
157
+ * mutation is the package's own, a `reduce` or an `edit` taking exactly
158
+ * one argument; a `patch` mutation takes a patch, not a value, so it
159
+ * cannot be fed.
160
+ *
161
+ * @param def - The mutation: `e3.mutation.reduce(...)` or `e3.mutation.edit(...)`
162
+ * @param spec - One file (`key`), or the newest under a prefix (`prefix`, `include`)
163
+ * @returns The mutation, as given
164
+ * @throws {FilesDeclarationError} When the mutation takes other than one argument, or is a `patch`
165
+ */
166
+ mutation(def, spec) {
167
+ const record = def.record.name;
168
+ const target = variant('mutation', { record, mutation: def.name });
169
+ const problems = [...nameProblems('record', record), ...nameProblems('mutation', def.name)];
170
+ if (def.form === 'patch')
171
+ problems.push(variant('mutation_patch', { record, mutation: def.name }));
172
+ else if (def.argTypes.length !== 1)
173
+ problems.push(variant('mutation_arity', { record, mutation: def.name, arity: BigInt(def.argTypes.length) }));
174
+ refuse(problems);
175
+ const { shape, problems: specProblems } = oneFileShape(target, spec, def.argTypes[0]);
176
+ this.#declare(target, shape, specProblems);
177
+ return def;
134
178
  }
135
179
  }
136
180
  /** Declaring files. */
@@ -139,7 +183,7 @@ export const Files = {
139
183
  * A source of files, which each workspace binds to a bucket and a prefix.
140
184
  *
141
185
  * @param name - Its name, unique among a solution's sources
142
- * @returns The source, whose methods declare the inputs it fills
186
+ * @returns The source, whose methods declare what its files fill
143
187
  * @throws {FilesDeclarationError} When the name is one e3 refuses
144
188
  */
145
189
  source(name) {
@@ -147,17 +191,17 @@ export const Files = {
147
191
  },
148
192
  /**
149
193
  * The declarations `e3-cloud files deploy` deploys: the sources, and the
150
- * inputs their files fill.
194
+ * inputs, records and mutations their files fill.
151
195
  *
152
196
  * @param sources - Every source
153
197
  * @returns The manifest, an East value of `FilesManifestType`
154
198
  * @throws {FilesDeclarationError} When two sources share a name, or two
155
- * mappings fill one input
199
+ * mappings fill one target
156
200
  */
157
201
  manifest(sources) {
158
202
  const problems = [];
159
203
  const names = new Set();
160
- const inputs = new Set();
204
+ const targets = [];
161
205
  const mappings = [];
162
206
  for (const source of sources) {
163
207
  if (names.has(source.name)) {
@@ -165,9 +209,9 @@ export const Files = {
165
209
  }
166
210
  names.add(source.name);
167
211
  for (const mapping of source.mappings) {
168
- if (inputs.has(mapping.input))
169
- problems.push(variant('duplicate_input', { input: mapping.input }));
170
- inputs.add(mapping.input);
212
+ if (targets.some((t) => sameFilesTarget(t, mapping.target)))
213
+ problems.push(variant('duplicate_target', { target: mapping.target }));
214
+ targets.push(mapping.target);
171
215
  mappings.push(mapping);
172
216
  }
173
217
  }