@fougere/adapter-file 0.6.0-alpha.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/src/index.ts +105 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fougere contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @fougere/adapter-file
|
|
2
|
+
|
|
3
|
+
> One JSON per row, a directory per entity
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// fougere.config.ts
|
|
7
|
+
export default defineFougere({
|
|
8
|
+
db: { path: '.data/app.db' },
|
|
9
|
+
sources: {
|
|
10
|
+
archive: { source: 'file', path: './rows', entities: ['Snapshot'] },
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`Snapshot` now reads and writes under `./rows/snapshot/<key>.json`; everything else stays in
|
|
16
|
+
`db`. The storage a handler receives is unchanged — where rows live is a fact about the app,
|
|
17
|
+
not about the code that uses them.
|
|
18
|
+
|
|
19
|
+
## What it costs, before you find out
|
|
20
|
+
|
|
21
|
+
`all()` reads the whole directory, so `list` with a `where` or an `orderBy` filters in
|
|
22
|
+
memory. That is right for a few thousand rows held for their durability and wrong on a hot
|
|
23
|
+
read path — the same trade `@fougere/adapter-duckdb` documents for a page-sized read.
|
|
24
|
+
|
|
25
|
+
Two processes over one directory have no lock. SQLite has one; a filesystem does not.
|
|
26
|
+
|
|
27
|
+
A key becomes a filename, so a key that would leave its directory is refused by name.
|
|
28
|
+
|
|
29
|
+
## What it does not declare
|
|
30
|
+
|
|
31
|
+
No `transacted`: a directory has no unit of work. So a
|
|
32
|
+
[frame](https://fougere.dev/docs/business/together) spanning this source and another
|
|
33
|
+
compensates rather than transacting, and the boot says so:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
RateCard+Ledger|RateMirrorTogether — compensated: rateCard in 'archive', ledger in 'db' — no isolation
|
|
37
|
+
Account+LedgerTogether — transaction, source 'db'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Two frames in one app, two guarantees, and the handlers are the same either way.
|
|
41
|
+
|
|
42
|
+
`migrate` is a `mkdir` per entity, which is the whole shape a directory has. Nothing here
|
|
43
|
+
emits a constraint, so a `ref()` across sources costs nothing.
|
|
44
|
+
|
|
45
|
+
## Writing your own source
|
|
46
|
+
|
|
47
|
+
This package is 105 lines, half of them filesystem guards. `storageOver` (`@fougere/core`)
|
|
48
|
+
derives the thirteen gestures of the storage port from four, so a new source supplies where
|
|
49
|
+
rows are and nothing else:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
Sources.register('redis', (conf): Source => ({
|
|
53
|
+
storageFactory: storageOver((_entity, name) => redisRows(conf.url, name)),
|
|
54
|
+
name: conf.url,
|
|
55
|
+
}));
|
|
56
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Source } from '@fougere/core';
|
|
2
|
+
export interface FileSourceOptions {
|
|
3
|
+
/** The directory the entity directories sit under. */
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function setupFile(opts: FileSourceOptions): Source;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EAEgB,KAAK,MAAM,EACjC,MAAM,eAAe,CAAC;AAsDvB,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAczD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rows as files — one JSON per row, a directory per entity.
|
|
3
|
+
*
|
|
4
|
+
* The reason this package is short: `storageOver` derives the thirteen gestures from four,
|
|
5
|
+
* so a source that keeps rows somewhere else supplies only where. Nothing about pages,
|
|
6
|
+
* criteria or lifecycle stamps is written twice.
|
|
7
|
+
*
|
|
8
|
+
* What it costs, said here rather than discovered: `all()` reads the whole directory, so
|
|
9
|
+
* `list` with a `where` or an `orderBy` filters in memory. That is the same trade
|
|
10
|
+
* `@fougere/adapter-duckdb` documents for a page-sized read — fine for a few thousand rows
|
|
11
|
+
* held for their durability, wrong on a hot read path. And two processes over one directory
|
|
12
|
+
* have no lock: SQLite has one, a filesystem does not.
|
|
13
|
+
*
|
|
14
|
+
* No `transacted` — a directory has no unit of work, so a frame compensates and says so.
|
|
15
|
+
* `migrate` is a `mkdir` per entity, which is the whole shape a directory has.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { lowerFirst } from '@fougere/schema';
|
|
20
|
+
import { Sources, storageOver, } from '@fougere/core';
|
|
21
|
+
/** A key becomes a filename, so it may not leave the directory it addresses. */
|
|
22
|
+
function fileOf(dir, key) {
|
|
23
|
+
if (key.includes('/') || key.includes('\\') || key === '.' || key === '..') {
|
|
24
|
+
throw new Error(`source 'file': '${key}' cannot be a filename — a key becomes one here.`);
|
|
25
|
+
}
|
|
26
|
+
return join(dir, `${encodeURIComponent(key)}.json`);
|
|
27
|
+
}
|
|
28
|
+
/** One entity's rows, one file each, under `<root>/<entity>/`. */
|
|
29
|
+
function dirRows(root, name) {
|
|
30
|
+
const dir = join(root, name);
|
|
31
|
+
const read = async (file) => {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
client: dir,
|
|
43
|
+
get: (key) => read(fileOf(dir, key)),
|
|
44
|
+
has: async (key) => (await read(fileOf(dir, key))) !== undefined,
|
|
45
|
+
set: async (key, row) => {
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
await writeFile(fileOf(dir, key), JSON.stringify(row, null, 2), 'utf8');
|
|
48
|
+
},
|
|
49
|
+
delete: async (key) => {
|
|
50
|
+
const file = fileOf(dir, key);
|
|
51
|
+
if ((await read(file)) === undefined)
|
|
52
|
+
return false;
|
|
53
|
+
await rm(file);
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
all: async () => {
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = await readdir(dir);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === 'ENOENT')
|
|
63
|
+
return [];
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const rows = await Promise.all(names
|
|
67
|
+
.filter((file) => file.endsWith('.json'))
|
|
68
|
+
.map((file) => read(join(dir, file))));
|
|
69
|
+
return rows.filter((row) => row !== undefined);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function setupFile(opts) {
|
|
74
|
+
return {
|
|
75
|
+
storageFactory: storageOver((_entity, name) => dirRows(opts.path, name)),
|
|
76
|
+
name: opts.path,
|
|
77
|
+
// A directory IS the shape, so bringing it up to date is making it exist. `elsewhere`
|
|
78
|
+
// is not read: nothing here emits a constraint, so a cross-source `ref()` costs nothing.
|
|
79
|
+
migrate: async (view) => {
|
|
80
|
+
for (const frond of view.fronds) {
|
|
81
|
+
for (const entry of frond.entities) {
|
|
82
|
+
await mkdir(join(opts.path, lowerFirst(entry.name)), { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
Sources.register('file', (conf) => {
|
|
89
|
+
const path = conf.path;
|
|
90
|
+
if (!path) {
|
|
91
|
+
throw new Error("source 'file': no `path` — a directory is what it is told to open.");
|
|
92
|
+
}
|
|
93
|
+
return setupFile({ path });
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fougere/adapter-file",
|
|
3
|
+
"version": "0.6.0-alpha.0",
|
|
4
|
+
"description": "Rows as files — one JSON per row, in a directory per entity.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fougere",
|
|
7
|
+
"typescript",
|
|
8
|
+
"storage",
|
|
9
|
+
"filesystem",
|
|
10
|
+
"json"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/chok/fougere.git",
|
|
16
|
+
"directory": "packages/adapter/file"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@fougere/core": "0.6.0-alpha.0",
|
|
33
|
+
"@fougere/schema": "0.6.0-alpha.0",
|
|
34
|
+
"@fougere/adapter-memory": "0.6.0-alpha.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "rm -rf dist && tsc",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rows as files — one JSON per row, a directory per entity.
|
|
3
|
+
*
|
|
4
|
+
* The reason this package is short: `storageOver` derives the thirteen gestures from four,
|
|
5
|
+
* so a source that keeps rows somewhere else supplies only where. Nothing about pages,
|
|
6
|
+
* criteria or lifecycle stamps is written twice.
|
|
7
|
+
*
|
|
8
|
+
* What it costs, said here rather than discovered: `all()` reads the whole directory, so
|
|
9
|
+
* `list` with a `where` or an `orderBy` filters in memory. That is the same trade
|
|
10
|
+
* `@fougere/adapter-duckdb` documents for a page-sized read — fine for a few thousand rows
|
|
11
|
+
* held for their durability, wrong on a hot read path. And two processes over one directory
|
|
12
|
+
* have no lock: SQLite has one, a filesystem does not.
|
|
13
|
+
*
|
|
14
|
+
* No `transacted` — a directory has no unit of work, so a frame compensates and says so.
|
|
15
|
+
* `migrate` is a `mkdir` per entity, which is the whole shape a directory has.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { lowerFirst } from '@fougere/schema';
|
|
20
|
+
import {
|
|
21
|
+
Sources, storageOver,
|
|
22
|
+
type Row, type Rows, type Source, type SourceConfig, type SourceView,
|
|
23
|
+
} from '@fougere/core';
|
|
24
|
+
|
|
25
|
+
/** A key becomes a filename, so it may not leave the directory it addresses. */
|
|
26
|
+
function fileOf(dir: string, key: string): string {
|
|
27
|
+
if (key.includes('/') || key.includes('\\') || key === '.' || key === '..') {
|
|
28
|
+
throw new Error(`source 'file': '${key}' cannot be a filename — a key becomes one here.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return join(dir, `${encodeURIComponent(key)}.json`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One entity's rows, one file each, under `<root>/<entity>/`. */
|
|
35
|
+
function dirRows(root: string, name: string): Rows {
|
|
36
|
+
const dir = join(root, name);
|
|
37
|
+
const read = async (file: string): Promise<Row | undefined> => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(await readFile(file, 'utf8')) as Row;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if ((error as { code?: string }).code === 'ENOENT') return undefined;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
client: dir,
|
|
48
|
+
get: (key) => read(fileOf(dir, key)),
|
|
49
|
+
has: async (key) => (await read(fileOf(dir, key))) !== undefined,
|
|
50
|
+
set: async (key, row) => {
|
|
51
|
+
await mkdir(dir, { recursive: true });
|
|
52
|
+
await writeFile(fileOf(dir, key), JSON.stringify(row, null, 2), 'utf8');
|
|
53
|
+
},
|
|
54
|
+
delete: async (key) => {
|
|
55
|
+
const file = fileOf(dir, key);
|
|
56
|
+
if ((await read(file)) === undefined) return false;
|
|
57
|
+
await rm(file);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
all: async () => {
|
|
61
|
+
let names: string[];
|
|
62
|
+
try {
|
|
63
|
+
names = await readdir(dir);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if ((error as { code?: string }).code === 'ENOENT') return [];
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
const rows = await Promise.all(names
|
|
69
|
+
.filter((file) => file.endsWith('.json'))
|
|
70
|
+
.map((file) => read(join(dir, file))));
|
|
71
|
+
|
|
72
|
+
return rows.filter((row): row is Row => row !== undefined);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface FileSourceOptions {
|
|
78
|
+
/** The directory the entity directories sit under. */
|
|
79
|
+
path: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function setupFile(opts: FileSourceOptions): Source {
|
|
83
|
+
return {
|
|
84
|
+
storageFactory: storageOver((_entity, name) => dirRows(opts.path, name)),
|
|
85
|
+
name: opts.path,
|
|
86
|
+
// A directory IS the shape, so bringing it up to date is making it exist. `elsewhere`
|
|
87
|
+
// is not read: nothing here emits a constraint, so a cross-source `ref()` costs nothing.
|
|
88
|
+
migrate: async (view: SourceView) => {
|
|
89
|
+
for (const frond of view.fronds) {
|
|
90
|
+
for (const entry of frond.entities) {
|
|
91
|
+
await mkdir(join(opts.path, lowerFirst(entry.name)), { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Sources.register('file', (conf: SourceConfig): Source => {
|
|
99
|
+
const path = conf.path as string | undefined;
|
|
100
|
+
if (!path) {
|
|
101
|
+
throw new Error("source 'file': no `path` — a directory is what it is told to open.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return setupFile({ path });
|
|
105
|
+
});
|