@t4h.framework/fs 0.13.0 → 0.14.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/.ai/skills/framework-fs/SKILL.md +87 -186
- package/.ai/skills/framework-fs-split/SKILL.md +134 -314
- package/.ai/skills/framework-fs-write/SKILL.md +54 -214
- package/CHANGELOG.md +56 -0
- package/README.md +20 -351
- package/dist/activities/FileJoinActivity.d.ts +4 -5
- package/dist/activities/FileJoinActivity.d.ts.map +1 -1
- package/dist/activities/FileJoinActivity.js +38 -43
- package/dist/activities/FileJoinActivity.js.map +1 -1
- package/dist/activities/FileReadActivity.d.ts +1 -0
- package/dist/activities/FileReadActivity.d.ts.map +1 -1
- package/dist/activities/FileReadActivity.js +34 -26
- package/dist/activities/FileReadActivity.js.map +1 -1
- package/dist/activities/FileSplitActivity.d.ts +23 -43
- package/dist/activities/FileSplitActivity.d.ts.map +1 -1
- package/dist/activities/FileSplitActivity.js +23 -38
- package/dist/activities/FileSplitActivity.js.map +1 -1
- package/dist/constants/operations.d.ts +1 -1
- package/dist/constants/operations.d.ts.map +1 -1
- package/dist/constants/operations.js +3 -14
- package/dist/constants/operations.js.map +1 -1
- package/dist/fs.d.ts +5 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/helpers/CSV.d.ts +2 -2
- package/dist/helpers/CSV.d.ts.map +1 -1
- package/dist/helpers/CSV.js +20 -22
- package/dist/helpers/CSV.js.map +1 -1
- package/dist/helpers/group-by-key.d.ts +8 -0
- package/dist/helpers/group-by-key.d.ts.map +1 -0
- package/dist/helpers/group-by-key.js +35 -0
- package/dist/helpers/group-by-key.js.map +1 -0
- package/dist/helpers/group-records.d.ts +2 -2
- package/dist/helpers/group-records.d.ts.map +1 -1
- package/dist/helpers/group-records.js +3 -3
- package/dist/helpers/group-records.js.map +1 -1
- package/dist/helpers/in-chunks.d.ts +5 -0
- package/dist/helpers/in-chunks.d.ts.map +1 -0
- package/dist/helpers/in-chunks.js +17 -0
- package/dist/helpers/in-chunks.js.map +1 -0
- package/dist/helpers/split-records.d.ts +4 -3
- package/dist/helpers/split-records.d.ts.map +1 -1
- package/dist/helpers/split-records.js +75 -46
- package/dist/helpers/split-records.js.map +1 -1
- package/dist/helpers/where-matcher.d.ts +21 -0
- package/dist/helpers/where-matcher.d.ts.map +1 -0
- package/dist/helpers/where-matcher.js +11 -0
- package/dist/helpers/where-matcher.js.map +1 -0
- package/dist/models/BinaryRecordKey.d.ts +27 -0
- package/dist/models/BinaryRecordKey.d.ts.map +1 -0
- package/dist/models/BinaryRecordKey.js +52 -0
- package/dist/models/BinaryRecordKey.js.map +1 -0
- package/dist/models/BinarySource.d.ts +45 -0
- package/dist/models/BinarySource.d.ts.map +1 -0
- package/dist/models/BinarySource.js +58 -0
- package/dist/models/BinarySource.js.map +1 -0
- package/dist/models/BinarySplit.d.ts +45 -0
- package/dist/models/BinarySplit.d.ts.map +1 -0
- package/dist/models/BinarySplit.js +81 -0
- package/dist/models/BinarySplit.js.map +1 -0
- package/dist/models/BinarySplitInput.d.ts +21 -0
- package/dist/models/BinarySplitInput.d.ts.map +1 -0
- package/dist/models/BinarySplitInput.js +85 -0
- package/dist/models/BinarySplitInput.js.map +1 -0
- package/dist/models/Database.d.ts +15 -0
- package/dist/models/Database.d.ts.map +1 -0
- package/dist/models/Database.js +57 -0
- package/dist/models/Database.js.map +1 -0
- package/dist/models/OpenKeyGroup.d.ts +23 -0
- package/dist/models/OpenKeyGroup.d.ts.map +1 -0
- package/dist/models/OpenKeyGroup.js +42 -0
- package/dist/models/OpenKeyGroup.js.map +1 -0
- package/dist/models/SplitDatabaseWorker.d.ts +12 -0
- package/dist/models/SplitDatabaseWorker.d.ts.map +1 -0
- package/dist/models/SplitDatabaseWorker.js +65 -0
- package/dist/models/SplitDatabaseWorker.js.map +1 -0
- package/dist/workers/split-database.d.ts +10 -0
- package/dist/workers/split-database.d.ts.map +1 -0
- package/dist/workers/split-database.js +43 -0
- package/dist/workers/split-database.js.map +1 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,367 +1,36 @@
|
|
|
1
1
|
# @t4h.framework/fs
|
|
2
2
|
|
|
3
|
-
File
|
|
4
|
-
|
|
5
|
-
workflow per
|
|
6
|
-
|
|
7
|
-
string, and `write` turns content the workflow produced into a file.
|
|
3
|
+
File functions for T4H Framework workflows. `read` brings a small byte window
|
|
4
|
+
into the workflow, `write` turns workflow content into a file, `split` starts
|
|
5
|
+
one child workflow per record of a file, and `join` folds the batch results
|
|
6
|
+
back into one file. Only references to the files go into the workflow history.
|
|
8
7
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
### FileSystemClaim
|
|
12
|
-
|
|
13
|
-
Abstract dependency for file system operations. An activity declares this claim when it needs to read or write files. The runtime provides a concrete implementation (e.g. local disk, S3, GCS).
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### Interface
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
abstract class FileSystemClaim {
|
|
23
|
-
public abstract write(
|
|
24
|
-
path: string,
|
|
25
|
-
readable: Readable | Buffer,
|
|
26
|
-
contentType?: string,
|
|
27
|
-
): Binary | Promise<Binary>
|
|
28
|
-
public abstract read(path: string): Binary | Promise<Binary>
|
|
29
|
-
|
|
30
|
-
public abstract slice(
|
|
31
|
-
binary: Binary,
|
|
32
|
-
range: { start: number; end: number },
|
|
33
|
-
encoding?: BufferEncoding,
|
|
34
|
-
): Binary | Promise<Binary>
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Usage inside an activity
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
import { SyncActivity, Claim } from '@t4h.framework/core'
|
|
42
|
-
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
43
|
-
|
|
44
|
-
class SaveReportActivity extends SyncActivity<
|
|
45
|
-
[data: Buffer],
|
|
46
|
-
{ path: string },
|
|
47
|
-
any
|
|
48
|
-
> {
|
|
49
|
-
private readonly claims = new Claim({
|
|
50
|
-
fs: FileSystemClaim,
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
public async toInput(data: Buffer) {
|
|
54
|
-
return { path: 'reports/output.pdf' }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public async run(input: { path: string }) {
|
|
58
|
-
const binary = await this.claims.fs.write(input.path, Buffer.from('...'))
|
|
59
|
-
return { contentType: binary.contentType }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
public toOutput(output: any) {
|
|
63
|
-
return output
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Runtime provider
|
|
69
|
-
|
|
70
|
-
The runtime must supply a concrete implementation:
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
{ provide: FileSystemClaim, value: new LocalFileSystemClaimImpl() }
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## File read
|
|
77
|
-
|
|
78
|
-
### read
|
|
79
|
-
|
|
80
|
-
Reads a byte window of a `Binary` into the workflow as a decoded string. The
|
|
81
|
-
output is recorded in the workflow history (replay never re-reads the file),
|
|
82
|
-
which is why reads are capped at 1 MiB: this is the primitive for headers,
|
|
83
|
-
trailers and single records — a data file flows through `split`, never through
|
|
84
|
-
`read`.
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { read } from '@t4h.framework/fs'
|
|
88
|
-
|
|
89
|
-
const header = await read(file, { range: { start: 0, end: 240 } })
|
|
90
|
-
|
|
91
|
-
const trailer = await read(file, {
|
|
92
|
-
range: { start: file.contentLength - 240, end: file.contentLength },
|
|
93
|
-
})
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Options: `range` (default: the whole binary, subject to the same cap) and
|
|
97
|
-
`encoding` (default `'utf8'`).
|
|
98
|
-
|
|
99
|
-
The activity tries `FileSystemClaim.slice` first — on a file-backed binary a
|
|
100
|
-
trailer read is a ranged download, whatever the file size. Binaries the claim
|
|
101
|
-
cannot slice (e.g. an SFTP reference) fall back to streaming: the stream is
|
|
102
|
-
consumed up to the window and destroyed as soon as it is filled, so a header
|
|
103
|
-
read pulls the first chunks only, whatever the file size. The one expensive
|
|
104
|
-
case is a trailing window over a non-sliceable binary — the stream must
|
|
105
|
-
traverse the whole file to reach it.
|
|
106
|
-
|
|
107
|
-
Reading a slice is relative to the slice: `read(slice, { range: { start: 0,
|
|
108
|
-
end: 10 } })` returns the first 10 bytes of the slice, not of the underlying
|
|
109
|
-
file — slice ranges compose.
|
|
110
|
-
|
|
111
|
-
## File write
|
|
112
|
-
|
|
113
|
-
### write
|
|
114
|
-
|
|
115
|
-
Stores content produced inside the workflow and returns its `Binary` — the only
|
|
116
|
-
supported way to obtain a `Binary` from a string or a `Buffer`:
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { write } from '@t4h.framework/fs'
|
|
120
|
-
|
|
121
|
-
const file = await write(JSON.stringify(state, null, 2), {
|
|
122
|
-
contentType: 'application/json',
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
await sftp.put('/out/state.json', file)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Options: `contentType` (default: inferred from the filename, then
|
|
129
|
-
`application/octet-stream`), `filename` (default: the activity id) and
|
|
130
|
-
`encoding` for a string content (default `'utf8'`; a `Buffer` always travels as
|
|
131
|
-
base64).
|
|
132
|
-
|
|
133
|
-
**Never implement `Binary` in an App.** Passing a value to a primitive makes it
|
|
134
|
-
the input of a durable activity, and that input is serialized to storage before
|
|
135
|
-
the activity runs — a `Binary` holding bytes in memory has nothing to serialize
|
|
136
|
-
into, and the activity may run in another process after a replay. A hand-rolled
|
|
137
|
-
class can pass locally (the dev server keeps values in memory) and fail once
|
|
138
|
-
deployed.
|
|
139
|
-
|
|
140
|
-
`write` takes content already in workflow memory — a state file, a report, a
|
|
141
|
-
return file. To build a large file out of many records, use `split` + `join`,
|
|
142
|
-
which stream through storage.
|
|
143
|
-
|
|
144
|
-
## File split
|
|
145
|
-
|
|
146
|
-
### split
|
|
147
|
-
|
|
148
|
-
Splits a file into slices, starts one child workflow per slice as
|
|
149
|
-
a single batch, and suspends the parent until every child is terminal —
|
|
150
|
-
regardless of each child succeeding or failing (a rejected child never
|
|
151
|
-
rejects the batch):
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { split, Split } from '@t4h.framework/fs'
|
|
155
|
-
|
|
156
|
-
const processRecord = new Workflow(
|
|
157
|
-
{ id: 'process-record', schema: Type.String() },
|
|
158
|
-
async (record: string) => {
|
|
159
|
-
// record is the raw slice content
|
|
160
|
-
},
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
const main = new Workflow(fileSchema, async (file: Binary) => {
|
|
164
|
-
const batch = await split(file, processRecord, {
|
|
165
|
-
delimiter: Split.line(),
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// this line only runs once the whole batch is terminal;
|
|
169
|
-
// batch: WorkflowBatchRef — { id: string, count: number }
|
|
170
|
-
})
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
The source is read once, as a stream:
|
|
174
|
-
|
|
175
|
-
1. The source is read once, worker side: cut into records, filtered by
|
|
176
|
-
`where` when given, grouped by `every`, and each slice goes into the batch
|
|
177
|
-
as a string as soon as it is complete. Nothing collects the slices, and a
|
|
178
|
-
slice may take up to 3 MiB: past that the split fails
|
|
179
|
-
while it reads, at the byte the slice starts.
|
|
180
|
-
2. The runtime reads that stream once and stores the batch as it goes; no copy
|
|
181
|
-
of the file is written and no slice is read back by range.
|
|
182
|
-
3. The activity output is just the batch reference (`{ id, count }`), so the
|
|
183
|
-
parent history stays O(1) whatever the slice count. Per-child results are
|
|
184
|
-
never aggregated: they live with each child run, to be inspected
|
|
185
|
-
individually through the batch id.
|
|
186
|
-
|
|
187
|
-
Child workflows declare their real input type (e.g. `Type.String()`); how the
|
|
188
|
-
bytes reach them is a runtime concern.
|
|
189
|
-
|
|
190
|
-
### Delimiters
|
|
191
|
-
|
|
192
|
-
The cut is defined by the dev through a serializable delimiter spec. When the
|
|
193
|
-
delimiter is found at `[p, p + length)`, the previous slice ends at
|
|
194
|
-
`p + keep.previous` and the next slice starts at `p + length - keep.next`; the
|
|
195
|
-
bytes in between are discarded. The defaults discard the whole delimiter
|
|
196
|
-
(classic line semantics).
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
Split.line() // cut at every '\n', delimiter discarded
|
|
200
|
-
Split.char(';') // cut at any byte sequence
|
|
201
|
-
Split.char(';', { previous: 1 }) // keep the ';' with the previous slice
|
|
202
|
-
Split.char('\n10', { next: 2 }) // positional files, see below
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The last form targets positional files where a logical record spans multiple
|
|
206
|
-
physical lines and a new record only starts when a line begins with a marker
|
|
207
|
-
(the record type code):
|
|
8
|
+
## Install
|
|
208
9
|
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
└──────────── record 1 ───────────┘└─────── record 2 ──────┘
|
|
10
|
+
```bash
|
|
11
|
+
npm install @t4h.framework/fs
|
|
212
12
|
```
|
|
213
13
|
|
|
214
|
-
|
|
215
|
-
discarded, and the `10` marker — which is data, not separator — stays at the
|
|
216
|
-
start of the next slice.
|
|
14
|
+
Peer dependency: `@t4h.framework/core`.
|
|
217
15
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
await split(file, workflow, {
|
|
222
|
-
delimiter: Split.line(), // required: where to cut
|
|
223
|
-
every: 10_000, // cut only every N occurrences (groups records)
|
|
224
|
-
trim: { start: 241, end: 241 }, // bytes cut off each end before scanning
|
|
225
|
-
skipEmpty: false, // keep zero-length slices (default: skipped)
|
|
226
|
-
encoding: 'latin1', // delimiter bytes + slice decoding (default: utf8)
|
|
227
|
-
timeoutInMs: 3_600_000, // max ms to await the batch; rejects on expiry
|
|
228
|
-
})
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
- **`every`** groups N records per child. With one million lines, `every:
|
|
232
|
-
10_000` starts 100 children instead of one million; each child receives a
|
|
233
|
-
raw block with its inner delimiters untouched and iterates locally. `keep`
|
|
234
|
-
arithmetic only applies at actual cut points.
|
|
235
|
-
- **`trim`** cuts a fixed number of bytes off each end of the file before
|
|
236
|
-
scanning — a positional header/trailer, which may not be aligned with the
|
|
237
|
-
record delimiter (a record-start delimiter like `Split.char('\n10')` fuses
|
|
238
|
-
the trailer into the last record's slice, so counting records would drop a
|
|
239
|
-
valid one along with it). Trimmed content never becomes a slice nor counts
|
|
240
|
-
toward `every`; fetch it with `read`. Getting the byte counts right is the
|
|
241
|
-
caller's business — positional layouts make them constants:
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
const header = await read(file, { range: { start: 0, end: 240 } })
|
|
245
|
-
|
|
246
|
-
const batch = await split(file, processInvoice, {
|
|
247
|
-
delimiter: Split.char('\n10', { next: 2 }),
|
|
248
|
-
trim: { start: 241, end: 241 }, // header + '\n', trailer + '\n'
|
|
249
|
-
})
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
- **`skipEmpty`** (default `true`) drops zero-length slices — consecutive
|
|
253
|
-
delimiters, or a file starting/ending with one — so they never become child
|
|
254
|
-
workflows. Turn it off when the slice index must align 1:1 with the record
|
|
255
|
-
position in the source file. "Empty" means zero bytes: a whitespace-only
|
|
256
|
-
line is a regular slice.
|
|
257
|
-
- **`encoding`** converts the delimiter to bytes and decodes each slice into
|
|
258
|
-
the string its child receives. Offsets are always computed over raw bytes,
|
|
259
|
-
so multi-byte encodings never skew the cuts.
|
|
260
|
-
- **`timeoutInMs`** is the maximum time in milliseconds to await the batch. On
|
|
261
|
-
expiry the activity rejects (reason `timeout`) instead of suspending
|
|
262
|
-
forever — the safety net for a child that never reaches a terminal state.
|
|
263
|
-
|
|
264
|
-
## File join
|
|
265
|
-
|
|
266
|
-
The inverse of `split`: folds the results of a completed batch into a single
|
|
267
|
-
file. Outputs stream in one at a time through `WorkflowBatchClaim` and flow
|
|
268
|
-
straight into `FileSystemClaim.write` with backpressure, so memory stays
|
|
269
|
-
bounded whatever the batch size — only the resulting binary reaches the
|
|
270
|
-
workflow history.
|
|
16
|
+
## Usage
|
|
271
17
|
|
|
272
18
|
```typescript
|
|
273
19
|
import { join, split, Split } from '@t4h.framework/fs'
|
|
274
20
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'date', // shorthand: key === header
|
|
283
|
-
],
|
|
284
|
-
})
|
|
285
|
-
// csv: Binary
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Formats
|
|
289
|
-
|
|
290
|
-
- **`lines`** — concatenates raw child outputs; the child owns the line
|
|
291
|
-
format. Options: `header`, `separator` (default `'\n'`).
|
|
292
|
-
- **`entries`** — NDJSON dump of `{ index, status, output }`, one entry per
|
|
293
|
-
line. The format-agnostic view of the batch, rejected children included by
|
|
294
|
-
default.
|
|
295
|
-
- **`csv`** — projects object outputs into RFC 4180 escaped columns. Options:
|
|
296
|
-
`columns` (required; each entry is `{ key, header? }` where `key` selects
|
|
297
|
-
the value on the child output — a dot-path like `'meta.total'` reaches
|
|
298
|
-
nested fields, matching the `where` semantics; a literal top-level key that
|
|
299
|
-
contains dots wins over the nested path — and `header` is the label on the
|
|
300
|
-
header row — a plain string is shorthand for `{ key }`), `delimiter`
|
|
301
|
-
(default `','`), `header` (default `true`; emits the column labels as the
|
|
302
|
-
first row).
|
|
303
|
-
|
|
304
|
-
### Status filter
|
|
21
|
+
const main = new Workflow(
|
|
22
|
+
{ id: 'main', schema: fileSchema },
|
|
23
|
+
async (file: Binary) => {
|
|
24
|
+
const batch = await split(file, processRecord, {
|
|
25
|
+
delimiter: Split.line(),
|
|
26
|
+
timeoutInMs: 3_600_000,
|
|
27
|
+
})
|
|
305
28
|
|
|
306
|
-
|
|
307
|
-
error policy: it defaults to `['fulfilled']` for `lines` and `csv`, and to
|
|
308
|
-
both statuses for `entries`. Filtering is silent by design — a file of
|
|
309
|
-
rejected children for reprocessing is one call away:
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
const failures = await join(batch, { format: 'entries', status: ['rejected'] })
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Where filter
|
|
316
|
-
|
|
317
|
-
`where` narrows results with a [sift](https://github.com/crcn/sift.js)
|
|
318
|
-
(MongoDB-style) query over each entry's output, applied after the `status`
|
|
319
|
-
filter. The accepted subset is deliberate — only declarative, JSON-only
|
|
320
|
-
operators: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`, `$nin`,
|
|
321
|
-
`$exists`, `$not`, `$and`, `$or`, `$nor`, plus plain equality and dot-paths
|
|
322
|
-
for nested fields:
|
|
323
|
-
|
|
324
|
-
```typescript
|
|
325
|
-
const relevant = await join(batch, {
|
|
326
|
-
format: 'csv',
|
|
327
|
-
columns: ['id', 'total'],
|
|
328
|
-
where: {
|
|
329
|
-
valid: true, // plain strict equality
|
|
330
|
-
total: { $gt: 100 }, // operators
|
|
331
|
-
'meta.situacao': 'aprovada', // dot-path into nested output
|
|
332
|
-
$or: [{ ref: 'a' }, { ref: 'b' }],
|
|
29
|
+
return await join(batch, { format: 'lines', header: 'id;total' })
|
|
333
30
|
},
|
|
334
|
-
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
`join` also accepts the child output type as a generic — top-level keys then
|
|
338
|
-
narrow to its fields, with values typed by the field (dot-paths are accepted
|
|
339
|
-
without narrowing):
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
type InvoiceResult = { id: string; valid: boolean; total: number }
|
|
343
|
-
|
|
344
|
-
await join<InvoiceResult>(batch, {
|
|
345
|
-
format: 'csv',
|
|
346
|
-
columns: ['id'],
|
|
347
|
-
where: { valid: true, total: { $gt: 100 } }, // { valid: 'yes' } rejects
|
|
348
|
-
})
|
|
31
|
+
)
|
|
349
32
|
```
|
|
350
33
|
|
|
351
|
-
|
|
352
|
-
rejected eagerly at `toInput` with a `TypeError`: functions (including
|
|
353
|
-
`$where`), `RegExp` values (including `$regex`), `undefined`, and operators
|
|
354
|
-
outside the subset — a dropped closure would make the replay filter
|
|
355
|
-
differently than the first execution. For arbitrary predicates, write a
|
|
356
|
-
custom activity in project code (the function then lives in versioned code,
|
|
357
|
-
not in history).
|
|
358
|
-
|
|
359
|
-
Two MongoDB semantics worth knowing: array fields match by containment
|
|
360
|
-
(`{ tags: 'a' }` matches `tags: ['a', 'b']`), and equality against an object
|
|
361
|
-
value compares deeply (never by reference). Comparisons never coerce types.
|
|
34
|
+
## Documentation
|
|
362
35
|
|
|
363
|
-
|
|
364
|
-
already hydrated one at a time — the query compiles once and adds one
|
|
365
|
-
predicate call per entry, no extra serialization and no change to the memory
|
|
366
|
-
profile. It is applied app-side: every child output is still fetched and
|
|
367
|
-
parsed; `where` decides what enters the file, not what leaves the database.
|
|
36
|
+
See [the fs documentation](https://tech4humans-brasil.github.io/framework/fs/).
|
|
@@ -26,15 +26,14 @@ export type FileJoinActivityInput = {
|
|
|
26
26
|
options: FileJoinOptions;
|
|
27
27
|
filter?: WorkflowBatchFilter;
|
|
28
28
|
};
|
|
29
|
-
export declare class FileJoinActivity extends SyncActivity<readonly [batch: WorkflowBatchCollectionRef
|
|
29
|
+
export declare class FileJoinActivity extends SyncActivity<readonly [batch: WorkflowBatchCollectionRef, options: FileJoinOptions], FileJoinActivityInput, Binary, Binary> {
|
|
30
30
|
private readonly claims;
|
|
31
|
-
toInput(batch: WorkflowBatchCollectionRef
|
|
31
|
+
toInput(batch: WorkflowBatchCollectionRef, options: FileJoinOptions): FileJoinActivityInput;
|
|
32
32
|
run({ batch, options, filter, }: FileJoinActivityInput): Promise<Binary>;
|
|
33
33
|
toOutput(output: Binary): Binary;
|
|
34
34
|
private toStatus;
|
|
35
|
-
private
|
|
36
|
-
private toLine;
|
|
35
|
+
private toLines;
|
|
37
36
|
}
|
|
38
|
-
export declare function join<TOutput = unknown>(batch: WorkflowBatchCollectionRef
|
|
37
|
+
export declare function join<TOutput = unknown>(batch: WorkflowBatchCollectionRef, options: FileJoinOptions<TOutput>): Promise<Binary>;
|
|
39
38
|
export {};
|
|
40
39
|
//# sourceMappingURL=FileJoinActivity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileJoinActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAGX,YAAY,EAEZ,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EAGxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAEvC,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAA;AAEhD,MAAM,MAAM,aAAa,CAAC,OAAO,GAAG,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE1E,KAAK,mBAAmB,CAAC,OAAO,IAAI;IAClC,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,KAAK,oBAAoB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAClE,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,sBAAsB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IACpE,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,KAAK,kBAAkB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAChE,MAAM,EAAE,KAAK,CAAA;IACb,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,OAAO,GAAG,OAAO,IACzC,oBAAoB,CAAC,OAAO,CAAC,GAC7B,sBAAsB,CAAC,OAAO,CAAC,GAC/B,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE/B,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,eAAe,CAAA;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAC7B,CAAA;AA2CD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,KAAK,EAAE,0BAA0B,EAAE,OAAO,EAAE,eAAe,CAAC,EACtE,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IAEK,OAAO,CACZ,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAeX,GAAG,CAAC,EACf,KAAK,EACL,OAAO,EACP,MAAM,GACP,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIvC,OAAO,CAAC,QAAQ;YAWD,OAAO;CAWvB;AAED,wBAAsB,IAAI,CAAC,OAAO,GAAG,OAAO,EAC1C,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,GAChC,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { PassThrough } from 'node:stream';
|
|
1
|
+
import { getDefaultHighWaterMark, Readable } from 'node:stream';
|
|
3
2
|
import { Claim, History, SyncActivity, WorkflowBatchClaim, WorkflowBatchQuery, } from '@t4h.framework/core';
|
|
4
3
|
import { FileSystemClaim } from '../FileSystemClaim.js';
|
|
5
4
|
import { CSV } from '../helpers/CSV.js';
|
|
6
|
-
const
|
|
7
|
-
lines:
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const formats = {
|
|
6
|
+
lines: {
|
|
7
|
+
extension: 'txt',
|
|
8
|
+
status: ['fulfilled'],
|
|
9
|
+
header: ({ header, separator = '\n' }) => header && header + separator,
|
|
10
|
+
line: ({ output }, { separator = '\n' }) => (typeof output === 'string' ? output : JSON.stringify(output)) +
|
|
11
|
+
separator,
|
|
12
|
+
},
|
|
13
|
+
entries: {
|
|
14
|
+
extension: 'ndjson',
|
|
15
|
+
status: ['fulfilled', 'rejected'],
|
|
16
|
+
line: entry => JSON.stringify(entry) + '\n',
|
|
17
|
+
},
|
|
18
|
+
csv: {
|
|
19
|
+
extension: 'csv',
|
|
20
|
+
status: ['fulfilled'],
|
|
21
|
+
header: ({ header, columns, delimiter }) => header === false ? undefined : CSV.toHeader(columns, { delimiter }),
|
|
22
|
+
line: ({ output }, { columns, delimiter }) => CSV.toLine(output, columns, { delimiter }),
|
|
23
|
+
},
|
|
10
24
|
};
|
|
25
|
+
function formatOf(options) {
|
|
26
|
+
return formats[options.format];
|
|
27
|
+
}
|
|
11
28
|
export class FileJoinActivity extends SyncActivity {
|
|
12
29
|
claims = new Claim({
|
|
13
30
|
fs: FileSystemClaim,
|
|
@@ -16,30 +33,20 @@ export class FileJoinActivity extends SyncActivity {
|
|
|
16
33
|
toInput(batch, options) {
|
|
17
34
|
if (options.where)
|
|
18
35
|
WorkflowBatchQuery.assertSerializable(options.where, 'where', 'Join where');
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
where: batch.where,
|
|
22
|
-
};
|
|
23
|
-
if (!filter.status && !filter.where)
|
|
36
|
+
const { status, where } = batch;
|
|
37
|
+
if (!status && !where)
|
|
24
38
|
return { batch: batch.id, options };
|
|
25
|
-
return { batch: batch.id, options, filter };
|
|
39
|
+
return { batch: batch.id, options, filter: { status, where } };
|
|
26
40
|
}
|
|
27
41
|
async run({ batch, options, filter, }) {
|
|
28
|
-
const stream = new PassThrough();
|
|
29
|
-
const writing = this.claims.fs.write(`join/${this.id}.${extensions[options.format]}`, stream);
|
|
30
|
-
const write = async (line) => {
|
|
31
|
-
if (!stream.write(line))
|
|
32
|
-
await once(stream, 'drain');
|
|
33
|
-
};
|
|
34
42
|
const entries = WorkflowBatchQuery.filter(this.claims.batch.outputs(batch), WorkflowBatchQuery.merge(filter, {
|
|
35
43
|
status: this.toStatus(options, filter),
|
|
36
44
|
where: options.where,
|
|
37
45
|
}));
|
|
38
|
-
await this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return await writing;
|
|
46
|
+
return await this.claims.fs.write(`join/${this.id}.${formatOf(options).extension}`, Readable.from(this.toLines(entries, options), {
|
|
47
|
+
objectMode: false,
|
|
48
|
+
highWaterMark: getDefaultHighWaterMark(false),
|
|
49
|
+
}));
|
|
43
50
|
}
|
|
44
51
|
toOutput(output) {
|
|
45
52
|
return output;
|
|
@@ -49,27 +56,15 @@ export class FileJoinActivity extends SyncActivity {
|
|
|
49
56
|
return options.status;
|
|
50
57
|
if (filter?.status)
|
|
51
58
|
return;
|
|
52
|
-
|
|
53
|
-
return ['fulfilled', 'rejected'];
|
|
54
|
-
return ['fulfilled'];
|
|
59
|
+
return formatOf(options).status;
|
|
55
60
|
}
|
|
56
|
-
async
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (options.format === 'entries')
|
|
64
|
-
return JSON.stringify(entry) + '\n';
|
|
65
|
-
if (options.format === 'csv')
|
|
66
|
-
return CSV.toLine(entry.output, options.columns, {
|
|
67
|
-
delimiter: options.delimiter,
|
|
68
|
-
});
|
|
69
|
-
const value = typeof entry.output === 'string'
|
|
70
|
-
? entry.output
|
|
71
|
-
: JSON.stringify(entry.output);
|
|
72
|
-
return value + (options.separator ?? '\n');
|
|
61
|
+
async *toLines(entries, options) {
|
|
62
|
+
const format = formatOf(options);
|
|
63
|
+
const header = format.header?.(options);
|
|
64
|
+
if (header)
|
|
65
|
+
yield header;
|
|
66
|
+
for await (const entry of entries)
|
|
67
|
+
yield format.line(entry, options);
|
|
73
68
|
}
|
|
74
69
|
}
|
|
75
70
|
export async function join(batch, options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileJoinActivity.js","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.js","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ export declare class FileReadActivity extends SyncActivity<readonly [binary: Bin
|
|
|
17
17
|
toOutput(output: string): string;
|
|
18
18
|
private slice;
|
|
19
19
|
private window;
|
|
20
|
+
private collect;
|
|
20
21
|
}
|
|
21
22
|
export declare function read(binary: Binary, options?: FileReadOptions): Promise<string>;
|
|
22
23
|
//# sourceMappingURL=FileReadActivity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileReadActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileReadActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAI/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,eAAe,CAAA;CACzB,CAAA;
|
|
1
|
+
{"version":3,"file":"FileReadActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileReadActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAI/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,eAAe,CAAA;CACzB,CAAA;AA0BD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,EACnD,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAIX,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAcnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;YAIzB,KAAK;YAYL,MAAM;YAeN,OAAO;CA2BtB;AAED,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
|
@@ -2,6 +2,18 @@ import { buffer } from 'node:stream/consumers';
|
|
|
2
2
|
import { Claim, History, SyncActivity } from '@t4h.framework/core';
|
|
3
3
|
import { FileSystemClaim } from '../FileSystemClaim.js';
|
|
4
4
|
const FILE_READ_MAX_BYTES = 1024 * 1024;
|
|
5
|
+
function assertRange(range, contentLength) {
|
|
6
|
+
const length = range.end - range.start;
|
|
7
|
+
if (!Number.isInteger(range.start) ||
|
|
8
|
+
!Number.isInteger(range.end) ||
|
|
9
|
+
range.start < 0 ||
|
|
10
|
+
length < 0)
|
|
11
|
+
throw new TypeError('Read range must be a non-negative byte window');
|
|
12
|
+
if (range.end > contentLength)
|
|
13
|
+
throw new TypeError('Read range must not exceed the binary length');
|
|
14
|
+
if (length > FILE_READ_MAX_BYTES)
|
|
15
|
+
throw new RangeError(`Length ${String(length)} exceeded max ${String(FILE_READ_MAX_BYTES)}`);
|
|
16
|
+
}
|
|
5
17
|
export class FileReadActivity extends SyncActivity {
|
|
6
18
|
claims = new Claim({
|
|
7
19
|
fs: FileSystemClaim,
|
|
@@ -12,13 +24,7 @@ export class FileReadActivity extends SyncActivity {
|
|
|
12
24
|
async run({ binary, options, }) {
|
|
13
25
|
const encoding = options.encoding ?? 'utf8';
|
|
14
26
|
const range = options.range ?? { start: 0, end: binary.contentLength };
|
|
15
|
-
|
|
16
|
-
if (!Number.isInteger(range.start) || range.start < 0 || length < 0)
|
|
17
|
-
throw new TypeError('Read range must be a non-negative byte window');
|
|
18
|
-
if (range.end > binary.contentLength)
|
|
19
|
-
throw new TypeError('Read range must not exceed the binary length');
|
|
20
|
-
if (length > FILE_READ_MAX_BYTES)
|
|
21
|
-
throw new RangeError(`Length ${length} exceeded max ${FILE_READ_MAX_BYTES}`);
|
|
27
|
+
assertRange(range, binary.contentLength);
|
|
22
28
|
const slice = await this.slice(binary, range, encoding);
|
|
23
29
|
if (slice)
|
|
24
30
|
return (await buffer(await slice.stream())).toString(encoding);
|
|
@@ -36,30 +42,32 @@ export class FileReadActivity extends SyncActivity {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
async window(readable, range) {
|
|
39
|
-
const chunks = [];
|
|
40
|
-
let position = 0;
|
|
41
|
-
let size = 0;
|
|
42
45
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (position <= range.start)
|
|
48
|
-
continue;
|
|
49
|
-
const from = Math.max(range.start - chunkStart, 0);
|
|
50
|
-
const to = Math.min(range.end - chunkStart, chunk.length);
|
|
51
|
-
chunks.push(chunk.subarray(from, to));
|
|
52
|
-
size += to - from;
|
|
53
|
-
if (position >= range.end)
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
46
|
+
const content = Buffer.concat(await this.collect(readable, range));
|
|
47
|
+
if (content.length < range.end - range.start)
|
|
48
|
+
throw new RangeError('Binary stream ended before the read window was filled');
|
|
49
|
+
return content;
|
|
56
50
|
}
|
|
57
51
|
finally {
|
|
58
52
|
readable.destroy();
|
|
59
53
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
}
|
|
55
|
+
async collect(readable, range) {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let position = 0;
|
|
58
|
+
for await (const data of readable) {
|
|
59
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
60
|
+
const chunkStart = position;
|
|
61
|
+
position += chunk.length;
|
|
62
|
+
if (position <= range.start)
|
|
63
|
+
continue;
|
|
64
|
+
const from = Math.max(range.start - chunkStart, 0);
|
|
65
|
+
const to = Math.min(range.end - chunkStart, chunk.length);
|
|
66
|
+
chunks.push(chunk.subarray(from, to));
|
|
67
|
+
if (position >= range.end)
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
return chunks;
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
export async function read(binary, options = {}) {
|