@t4h.framework/fs 0.13.1 → 0.14.2

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 (78) hide show
  1. package/.ai/skills/framework-fs/SKILL.md +87 -186
  2. package/.ai/skills/framework-fs-split/SKILL.md +134 -306
  3. package/.ai/skills/framework-fs-write/SKILL.md +54 -212
  4. package/CHANGELOG.md +37 -0
  5. package/README.md +29 -345
  6. package/dist/activities/FileJoinActivity.d.ts +1 -2
  7. package/dist/activities/FileJoinActivity.d.ts.map +1 -1
  8. package/dist/activities/FileJoinActivity.js +38 -43
  9. package/dist/activities/FileJoinActivity.js.map +1 -1
  10. package/dist/activities/FileReadActivity.d.ts.map +1 -1
  11. package/dist/activities/FileReadActivity.js +18 -15
  12. package/dist/activities/FileReadActivity.js.map +1 -1
  13. package/dist/activities/FileSplitActivity.d.ts +21 -34
  14. package/dist/activities/FileSplitActivity.d.ts.map +1 -1
  15. package/dist/activities/FileSplitActivity.js +23 -38
  16. package/dist/activities/FileSplitActivity.js.map +1 -1
  17. package/dist/constants/operations.d.ts.map +1 -1
  18. package/dist/constants/operations.js +3 -14
  19. package/dist/constants/operations.js.map +1 -1
  20. package/dist/fs.d.ts +5 -1
  21. package/dist/fs.d.ts.map +1 -1
  22. package/dist/helpers/CSV.d.ts +2 -2
  23. package/dist/helpers/CSV.d.ts.map +1 -1
  24. package/dist/helpers/CSV.js +20 -24
  25. package/dist/helpers/CSV.js.map +1 -1
  26. package/dist/helpers/group-by-key.d.ts +8 -0
  27. package/dist/helpers/group-by-key.d.ts.map +1 -0
  28. package/dist/helpers/group-by-key.js +35 -0
  29. package/dist/helpers/group-by-key.js.map +1 -0
  30. package/dist/helpers/group-records.d.ts +2 -2
  31. package/dist/helpers/group-records.d.ts.map +1 -1
  32. package/dist/helpers/group-records.js +3 -3
  33. package/dist/helpers/group-records.js.map +1 -1
  34. package/dist/helpers/in-chunks.d.ts +5 -0
  35. package/dist/helpers/in-chunks.d.ts.map +1 -0
  36. package/dist/helpers/in-chunks.js +17 -0
  37. package/dist/helpers/in-chunks.js.map +1 -0
  38. package/dist/helpers/split-records.d.ts +4 -3
  39. package/dist/helpers/split-records.d.ts.map +1 -1
  40. package/dist/helpers/split-records.js +60 -41
  41. package/dist/helpers/split-records.js.map +1 -1
  42. package/dist/helpers/where-matcher.d.ts +21 -0
  43. package/dist/helpers/where-matcher.d.ts.map +1 -0
  44. package/dist/helpers/where-matcher.js +11 -0
  45. package/dist/helpers/where-matcher.js.map +1 -0
  46. package/dist/models/BinaryRecordKey.d.ts +27 -0
  47. package/dist/models/BinaryRecordKey.d.ts.map +1 -0
  48. package/dist/models/BinaryRecordKey.js +52 -0
  49. package/dist/models/BinaryRecordKey.js.map +1 -0
  50. package/dist/models/BinarySource.d.ts +45 -0
  51. package/dist/models/BinarySource.d.ts.map +1 -0
  52. package/dist/models/BinarySource.js +58 -0
  53. package/dist/models/BinarySource.js.map +1 -0
  54. package/dist/models/BinarySplit.d.ts +45 -0
  55. package/dist/models/BinarySplit.d.ts.map +1 -0
  56. package/dist/models/BinarySplit.js +81 -0
  57. package/dist/models/BinarySplit.js.map +1 -0
  58. package/dist/models/BinarySplitInput.d.ts +21 -0
  59. package/dist/models/BinarySplitInput.d.ts.map +1 -0
  60. package/dist/models/BinarySplitInput.js +85 -0
  61. package/dist/models/BinarySplitInput.js.map +1 -0
  62. package/dist/models/Database.d.ts +15 -0
  63. package/dist/models/Database.d.ts.map +1 -0
  64. package/dist/models/Database.js +57 -0
  65. package/dist/models/Database.js.map +1 -0
  66. package/dist/models/OpenKeyGroup.d.ts +23 -0
  67. package/dist/models/OpenKeyGroup.d.ts.map +1 -0
  68. package/dist/models/OpenKeyGroup.js +42 -0
  69. package/dist/models/OpenKeyGroup.js.map +1 -0
  70. package/dist/models/SplitDatabaseWorker.d.ts +12 -0
  71. package/dist/models/SplitDatabaseWorker.d.ts.map +1 -0
  72. package/dist/models/SplitDatabaseWorker.js +65 -0
  73. package/dist/models/SplitDatabaseWorker.js.map +1 -0
  74. package/dist/workers/split-database.d.ts +10 -0
  75. package/dist/workers/split-database.d.ts.map +1 -0
  76. package/dist/workers/split-database.js +43 -0
  77. package/dist/workers/split-database.js.map +1 -0
  78. package/package.json +3 -3
@@ -1,240 +1,82 @@
1
1
  ---
2
2
  name: framework-fs-write
3
3
  description: >-
4
- Guides the use of `write` from @t4h.framework/fs — the only supported way to
5
- turn content produced inside a workflow (a JSON string, a CSV, a positional
6
- return file, a Buffer) into a `Binary` that other primitives accept. Use
7
- whenever a workflow must upload, send or save content it built itself:
8
- `sftp.put`/`sftp.append` with generated content, an HTTP request carrying a
9
- file, persisting a report, or when the user mentions creating/building a
10
- Binary, converting a string or Buffer to a Binary, "stringToBinary",
11
- "BufferBinary", or hits `Unsupported binary instance`. A workflow must NEVER
12
- implement its own `Binary` subclass — this skill explains why and what to do
13
- instead.
4
+ Shows how to turn content that a T4H Framework workflow built (a JSON
5
+ string, a CSV, a positional return file, a Buffer) into a Binary with
6
+ write() from @t4h.framework/fs, and how to test it. Use when a workflow must
7
+ upload, send or save content it made itself (sftp.put or sftp.append with
8
+ generated content, an HTTP request with a file body, a report), or when
9
+ someone asks to create a Binary, convert a string or a Buffer to a Binary,
10
+ writes a stringToBinary or BufferBinary helper, or extends Binary. A
11
+ workflow never implements its own Binary.
14
12
  ---
15
13
 
16
- # `write` — turn workflow content into a `Binary`
14
+ # write — workflow content into a Binary
17
15
 
18
- Every file primitive in the framework speaks `Binary`: `sftp.put`,
19
- `sftp.append`, `split`, `join`, `pdf`, HTTP bodies. A `Binary` is a
20
- **reference** to stored content, not the bytes themselves.
16
+ Read the skill `framework-fs` first for the `Binary` rules.
21
17
 
22
- `write` is what produces one from content your workflow built:
18
+ ## Write and upload
23
19
 
24
20
  ```typescript
25
21
  import { write } from '@t4h.framework/fs'
26
22
 
27
23
  const file = await write(JSON.stringify(state, null, 2), {
28
24
  contentType: 'application/json',
25
+ filename: 'state.json',
29
26
  })
30
27
 
31
28
  await sftp.put('/out/state.json', file)
32
29
  ```
33
30
 
34
- Package path: `framework/packages/fs`. Import only from the package root.
35
-
36
- ---
37
-
38
- ## Never implement `Binary` yourself
39
-
40
- `@t4h.framework/core/no-binary-subclass` rejects a class that extends `Binary`.
31
+ 1. Build the content as a `string` or a `Buffer` in the workflow.
32
+ 2. Call `write(content, { contentType })`. Set `contentType` when the
33
+ receiver cares. Without it, the claim infers one from `filename`, which
34
+ defaults to the activity id.
35
+ 3. Pass the `Binary` to the next primitive. One `Binary` can go to many
36
+ primitives (for example `sftp.put` to a `.tmp` path and then to the final
37
+ path); nothing is copied.
38
+
39
+ ## Traps
40
+
41
+ - Do not write a `Binary` subclass or a `stringToBinary` helper that wraps
42
+ bytes. Replace its body with `write`; keep the helper name if it reads well.
43
+ - The option is `filename`, not `path` or `name`. It is the storage name, not
44
+ the upload path.
45
+ - `encoding` applies only to a string `content` (default `'utf8'`). A
46
+ `Buffer` is written as it is.
47
+ - `write` takes only `string | Buffer`. A value that already is a `Binary`
48
+ goes straight to the next primitive.
49
+ - `write` is for content that fits in workflow memory: a state file, a
50
+ report, a return file. To build a large file from many records, use
51
+ `split` and `join` (skill `framework-fs-split`); never concatenate records in
52
+ the workflow.
53
+ - `write` is an activity. On replay it returns the recorded `Binary` and does
54
+ not write again.
55
+
56
+ ## Test
57
+
58
+ Mock `FileSystemClaim.write`. `write` passes the content to the claim as a
59
+ `Buffer`. Use the `TestBinary` double from the skill `framework-fs`:
41
60
 
42
61
  ```typescript
43
- // ❌ NEVER — this class cannot be persisted
44
- class BufferBinary extends Binary {
45
- constructor(
46
- private readonly content: Buffer,
47
- readonly contentType = '...',
48
- ) {
49
- super()
50
- }
51
-
52
- get contentLength() {
53
- return this.content.length
54
- }
55
-
56
- stream() {
57
- return Readable.from(this.content)
58
- }
59
- }
60
-
61
- await sftp.put('/out/state.json', new BufferBinary(Buffer.from(json)))
62
- ```
63
-
64
- ```typescript
65
- // ✅ ALWAYS
66
- await sftp.put(
67
- '/out/state.json',
68
- await write(json, {
69
- contentType: 'application/json',
70
- }),
62
+ const writeFile = vi.fn(
63
+ async (_path: string, content: Buffer, contentType?: string) =>
64
+ new TestBinary(content, contentType),
71
65
  )
72
- ```
73
-
74
- ### Why a custom `Binary` cannot work
75
-
76
- A workflow is **durable**: the body replays, and each activity is persisted
77
- before it runs. When you pass a value to a primitive, that value becomes the
78
- activity **input**, and the input is serialized to storage.
79
-
80
- A `Binary` holding bytes in memory has nothing to serialize into — the buffer
81
- lives in the process that built it, and the activity may run in another
82
- process, minutes later, after a replay. The runtime therefore has to spool the
83
- content somewhere before the activity can be persisted at all.
84
-
85
- Symptoms of doing it by hand:
86
-
87
- - `TypeError: Unsupported binary instance` when the activity input is
88
- serialized, on older runtimes;
89
- - the activity is never persisted, the transaction rolls back, and the message
90
- ends up in the dead letter queue after the retries — the run stops mid-way
91
- with no obvious error in the workflow itself;
92
- - the dev server also throws `TypeError: Unsupported binary instance` when it
93
- stores the activity input. It accepts only the `Binary` that its own claims
94
- return.
95
-
96
- Current runtimes spool an unknown `Binary` to storage instead of failing, so a
97
- hand-rolled class may survive — but it pays a hidden copy, loses the content
98
- type you meant to set, and depends on runtime-version behavior. `write` is the
99
- contract.
100
-
101
- ---
102
66
 
103
- ## API
104
-
105
- ```typescript
106
- function write(
107
- content: string | Buffer,
108
- options?: FileWriteOptions,
109
- ): Promise<Binary>
110
- ```
111
-
112
- | Option | Type | Required | Purpose |
113
- | ------------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
114
- | `contentType` | `string` | No | Content type stored with the file — `'application/json'`, `'text/csv'`, … Defaults to what the filename extension implies, then `application/octet-stream`. |
115
- | `filename` | `string` | No | Name the file is stored under. Defaults to the activity id. |
116
- | `encoding` | `BufferEncoding` | No | How a **string** `content` is encoded. Defaults to `'utf8'`. Ignored for a `Buffer`, which always travels as base64. |
117
-
118
- `write` is a durable activity: on replay the content is **not** written again;
119
- the recorded `Binary` is returned.
120
-
121
- ---
122
-
123
- ## When to use it
124
-
125
- | Situation | Use |
126
- | ----------------------------------------------------------------------------- | --------------------------------------------- |
127
- | Send content the workflow built (JSON, CSV, positional file) over SFTP/HTTP | `write` |
128
- | Read a small window of an existing file into the workflow | [`read`](./read) — see **framework-fs-split** |
129
- | Fan a large file out into one workflow per record | `split` — see **framework-fs-split** |
130
- | Fold a finished batch back into a file | `join` — see **framework-fs-split** |
131
- | Content already is a `Binary` (HTTP response body, `sftp.get`, `join` result) | pass it straight through |
132
-
133
- ---
134
-
135
- ## Size matters: keep content bounded
136
-
137
- `write` takes content **in workflow memory**, so it is bounded by what the
138
- workflow can reasonably hold — a state file, a report, a return file. It is not
139
- the tool for gigabyte outputs.
140
-
141
- To build a large file out of many records, use `join` over a batch: it streams
142
- results one at a time into storage, and nothing but the resulting `Binary`
143
- reaches the workflow.
144
-
145
- ---
146
-
147
- ## Patterns
148
-
149
- ### A) Write then upload
150
-
151
- ```typescript
152
- const csv = await write(rows.map(toCsvLine).join('\n'), {
153
- contentType: 'text/csv',
154
- filename: `report-${input.date}.csv`,
67
+ await TestWorkflowEnvironment.create().execute(workflow, input, {
68
+ claims: [mockClaim(FileSystemClaim, { write: writeFile })],
155
69
  })
156
70
 
157
- await sftp.put(`/out/report-${input.date}.csv`, csv)
158
- ```
159
-
160
- ### B) Write once, use twice
161
-
162
- The same `Binary` can be handed to more than one primitive — it is a reference,
163
- so nothing is copied and nothing is re-uploaded:
164
-
165
- ```typescript
166
- const file = await write(json, { contentType: 'application/json' })
167
-
168
- await sftp.put(`${path}.tmp`, file)
169
- await sftp.put(path, file)
170
- ```
171
-
172
- Note that a hand-rolled `Binary` backed by a single-use stream would break on
173
- the second call — another reason not to build one.
174
-
175
- ### C) Binary content
176
-
177
- ```typescript
178
- const pdfBytes: Buffer = await buildPdf()
179
-
180
- await sftp.put(
181
- '/out/invoice.pdf',
182
- await write(pdfBytes, {
183
- contentType: 'application/pdf',
184
- }),
71
+ expect(writeFile).toHaveBeenCalledWith(
72
+ 'report.csv',
73
+ expect.any(Buffer),
74
+ 'text/csv',
185
75
  )
186
76
  ```
187
77
 
188
- ---
189
-
190
- ## Testing
191
-
192
- `write` goes through `FileSystemClaim.write`, so mock that claim like any
193
- other. See the **framework-workflow-testing** skill for the harness API.
194
-
195
- ```typescript
196
- import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
197
- import { FileSystemClaim } from '@t4h.framework/fs'
198
-
199
- // replay runs the workflow twice — return a fresh Binary per call
200
- const write = vi
201
- .fn()
202
- .mockImplementation(
203
- async (path, content, contentType) =>
204
- new MockBinary(
205
- Buffer.isBuffer(content) ? content : await toBuffer(content),
206
- contentType,
207
- ),
208
- )
209
-
210
- await env.execute(workflow, input, {
211
- claims: [
212
- mockClaim(FileSystemClaim, { write, read: vi.fn(), slice: vi.fn() }),
213
- ],
214
- })
215
-
216
- expect(write).toHaveBeenCalledWith('report.csv', expect.any(Buffer), 'text/csv')
217
- ```
218
-
219
- ---
220
-
221
- ## Anti-patterns
222
-
223
- - Helper functions like `stringToBinary(value)` that wrap content in a custom
224
- class — replace the implementation with `write`, keeping the helper name if
225
- it reads well.
226
- - `write`-ing a value that already is a `Binary`. `write` takes only
227
- `string | Buffer`, so `tsc` rejects it.
228
- - Building a large file by concatenating records in memory and calling `write`
229
- once — use `split`/`join` instead.
230
- - Relying on the filename extension to set the content type when the consumer
231
- cares about it — pass `contentType` explicitly.
232
-
233
- ---
234
-
235
- ## Related skills
78
+ ## Reference
236
79
 
237
- - `framework-fs` — the `FileSystemClaim` contract and runtime providers.
238
- - `framework-fs-split` — `read`/`split`/`join`: windows, fan-out and fold.
239
- - `framework-sftp` — `sftp.put`/`append`, the most common consumer of `write`.
240
- - `framework-workflow-creation` — the rules a workflow body must follow.
80
+ - `write`: https://tech4humans-brasil.github.io/framework/fs/write
81
+ - Types: `node_modules/@t4h.framework/fs/dist/activities/FileWriteActivity.d.ts`
82
+ - Related skills: `framework-fs`, `framework-fs-split`, `framework-sftp`.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @t4h.framework/fs
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#276](https://github.com/tech4humans-brasil/framework/pull/276) [`7b38934`](https://github.com/tech4humans-brasil/framework/commit/7b38934e8f9207c98c33afcd76e3a7a4da7e8b14) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The README tells about `asset`, has a usage sample that compiles, and links
8
+ to the guide, the API reference and the changelog. The docs do not name the
9
+ packages or the storage behind a `Binary`, and they give the Node.js version
10
+ and the peer dependency.
11
+
12
+ ## 0.14.1
13
+
14
+ ## 0.14.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [#260](https://github.com/tech4humans-brasil/framework/pull/260) [`e9e8036`](https://github.com/tech4humans-brasil/framework/commit/e9e80366440b8da611489267dd02680592ca32e9) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - `split` gets two options. `groupBy` starts one child per key, with all the records of the key. `with` gives each child the rows of other files that have the same key. The key is a `match` `RegExp` with optional `replace` steps. The `input` template gets two more markers: `Split.key` and `Split.rows(name)`. `split` reads each file one time into a temporary SQLite file on the local disk, keeps only the rows of `with` whose key is in the main file, and removes the file at the end. The files can have 1 GiB in total. The package now needs Node 22.13 or later, the first version with `node:sqlite` and `iterate()` without a flag.
19
+
20
+ The types of `split` now have the `Binary` prefix: `BinarySplitOptions`, `BinarySourceOptions`, `BinaryRecordKeyOptions`, `BinaryReplace`, `BinaryDelimiter` and `BinaryWhere`, with a `JSON` form for the stored options. `FileSplitOptions` and `FileSplitActivityOptions` extend them and keep their shape. `FileSplitDelimiter` and `FileSplitWhere` are deprecated: use `BinaryDelimiter` and `BinaryWhere`.
21
+
22
+ - [#262](https://github.com/tech4humans-brasil/framework/pull/262) [`e46ebbb`](https://github.com/tech4humans-brasil/framework/commit/e46ebbbe1e711febd21ddcb920e890125fbb9f0a) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - `split` gets the `input` option: a template that each child gets, with its slice in place of the `Split.slice` marker. The other values of the template stay as they are. `tsc` checks the template against the schema of the child workflow, so `split` and `FileSplitOptions` take the child workflow as a type parameter. `FileSplitActivityInput` stores the template in `input`, and its `options` use the new `FileSplitActivityOptions` type, which has no `input`.
23
+
24
+ ### Patch Changes
25
+
26
+ - [#248](https://github.com/tech4humans-brasil/framework/pull/248) [`ae6d224`](https://github.com/tech4humans-brasil/framework/commit/ae6d22475ee07307ac59582581a6077788b902ca) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds the `no-batch-query-in-loop` rule to `@t4h.framework/core/eslint` and turns it on in `configs.recommended`, at the `error` level, for TypeScript files that are not tests. After the upgrade, a project that uses the recommended config can get new lint errors. The rule reports these calls in a loop or in the callback of an array method:
27
+ - `join`, `split` and `index` from `@t4h.framework/fs`, and `size()`, `take()` and `toArray()` on a `WorkflowBatchCollectionRef` (also after `.filter(...)`). Each call queries or scans the whole batch or file, so a loop does N scans.
28
+ - `read` from `@t4h.framework/fs`. Each call is a separate activity and a separate file read, and each result goes into the workflow history.
29
+
30
+ The rule checks all the functions of the file. In an activity class, it checks only the `constructor`, `toInput` and `toOutput`. It checks only the call site, not a function that the loop calls. The core docs and skills, and the `framework-fs-split` skill of `@t4h.framework/fs`, tell you to build an index one time and call `lookup` in the loop.
31
+
32
+ - [#253](https://github.com/tech4humans-brasil/framework/pull/253) [`d9f5640`](https://github.com/tech4humans-brasil/framework/commit/d9f56403f90ffa1e04640b244f8c9247a119c084) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The fs docs and skills no longer describe how the dev server or a runtime stores a hand-rolled `Binary`. They say that such a class can pass in one environment and fail in another.
33
+
34
+ - [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The fs site pages now hold the full contract, the README is short, and each skill gives only the steps and traps for a consumer app. The docs now name the `split` output `WorkflowBatchCollectionRef` and no longer name a runtime limit.
35
+
36
+ - [#258](https://github.com/tech4humans-brasil/framework/pull/258) [`421f163`](https://github.com/tech4humans-brasil/framework/commit/421f16390834a26b87da5936dfa7c30c669785ed) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - `join` now fails the file stream when the batch fails before the end. Before, the stream stayed open and the write of the host did not end. `read` now rejects a range with an `end` that is not an integer. The other changes are internal and the public API does not change.
37
+
38
+ - [#246](https://github.com/tech4humans-brasil/framework/pull/246) [`b99b41f`](https://github.com/tech4humans-brasil/framework/commit/b99b41f60d69b3037187e9fcdc27ab0d47ff18a2) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds byte windows to `SftpBinary`. `binary.slice(range)` returns a view of a byte window of the remote file that reuses the stream of `binary`. The new `range` field is absolute in the file, with an exclusive `end`, and `contentLength` is `range.end - range.start`. A slice of a slice composes. `toJSON()` includes `range` on a slice only, so the JSON of a whole file does not change. `slice` throws a `RangeError` when the window is not inside the binary. In `@t4h.framework/fs`, the docs no longer give an SFTP reference as an example of a binary that cannot be sliced.
39
+
3
40
  ## 0.13.1
4
41
 
5
42
  ### Patch Changes