json-fs-query 3.1.4 → 3.3.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/README.md +257 -15
- package/browser/jsonFsQuery.js +3884 -5057
- package/browser/jsonFsQuery.min.js +1 -1
- package/dist/data/introspect/alter.d.ts +12 -0
- package/dist/data/introspect/alter.js +304 -0
- package/dist/data/introspect/alter.min.js +1 -0
- package/dist/data/introspect/create.d.ts +10 -0
- package/dist/data/introspect/create.js +9 -23
- package/dist/data/introspect/create.min.js +1 -1
- package/dist/data/introspect/drop.js +7 -15
- package/dist/data/introspect/drop.min.js +1 -1
- package/dist/data/introspect/formatDatabase.d.ts +17 -0
- package/dist/data/introspect/formatDatabase.js +56 -0
- package/dist/data/introspect/formatDatabase.min.js +1 -0
- package/dist/data/introspect/index.d.ts +9 -0
- package/dist/data/introspect/index.js +18 -5
- package/dist/data/introspect/index.min.js +1 -1
- package/dist/data/introspect/packDatabase.d.ts +27 -0
- package/dist/data/introspect/packDatabase.js +113 -0
- package/dist/data/introspect/packDatabase.min.js +1 -0
- package/dist/data/introspect/policies/foreignKeyExists.d.ts +2 -0
- package/dist/data/introspect/policies/foreignKeyExists.js +9 -3
- package/dist/data/introspect/policies/foreignKeyExists.min.js +1 -1
- package/dist/data/introspect/policies/hasDependents.d.ts +3 -3
- package/dist/data/introspect/policies/hasDependents.js +11 -3
- package/dist/data/introspect/policies/hasDependents.min.js +1 -1
- package/dist/data/introspect/policies/keyUnique.d.ts +3 -0
- package/dist/data/introspect/policies/keyUnique.js +10 -0
- package/dist/data/introspect/policies/keyUnique.min.js +1 -1
- package/dist/data/queries/databaseStatements.d.ts +31 -0
- package/dist/data/queries/databaseStatements.js +97 -0
- package/dist/data/queries/databaseStatements.min.js +1 -0
- package/dist/data/queries/deleteEntity.js +35 -19
- package/dist/data/queries/deleteEntity.min.js +1 -1
- package/dist/data/queries/insertEntity.d.ts +8 -0
- package/dist/data/queries/insertEntity.js +340 -43
- package/dist/data/queries/insertEntity.min.js +1 -1
- package/dist/data/queries/joinEntity.d.ts +15 -0
- package/dist/data/queries/joinEntity.js +91 -15
- package/dist/data/queries/joinEntity.min.js +1 -1
- package/dist/data/queries/query.d.ts +2 -1
- package/dist/data/queries/query.js +5 -2
- package/dist/data/queries/query.min.js +1 -1
- package/dist/data/queries/selectFields.d.ts +5 -4
- package/dist/data/queries/selectFields.js +25 -11
- package/dist/data/queries/selectFields.min.js +1 -1
- package/dist/data/queries/updateEntity.js +54 -15
- package/dist/data/queries/updateEntity.min.js +1 -1
- package/dist/data/storage/databases.d.ts +32 -0
- package/dist/data/storage/databases.js +93 -0
- package/dist/data/storage/databases.min.js +1 -0
- package/dist/data/storage/fileExists.d.ts +7 -0
- package/dist/data/storage/fileExists.js +25 -0
- package/dist/data/storage/fileExists.min.js +1 -0
- package/dist/data/storage/index.d.ts +19 -0
- package/dist/data/storage/index.js +124 -0
- package/dist/data/storage/index.min.js +1 -0
- package/dist/data/storage/listFiles.d.ts +8 -0
- package/dist/data/storage/listFiles.js +52 -0
- package/dist/data/storage/listFiles.min.js +1 -0
- package/dist/data/storage/makeDirectory.d.ts +7 -0
- package/dist/data/storage/makeDirectory.js +24 -0
- package/dist/data/storage/makeDirectory.min.js +1 -0
- package/dist/data/storage/nodeFiles.d.ts +6 -0
- package/dist/data/storage/nodeFiles.js +12 -0
- package/dist/data/storage/nodeFiles.min.js +1 -0
- package/dist/data/storage/readCache.d.ts +37 -0
- package/dist/data/storage/readCache.js +117 -0
- package/dist/data/storage/readCache.min.js +1 -0
- package/dist/data/storage/readJson.d.ts +16 -0
- package/dist/data/storage/readJson.js +38 -0
- package/dist/data/storage/readJson.min.js +1 -0
- package/dist/data/storage/removeDirectory.d.ts +8 -0
- package/dist/data/storage/removeDirectory.js +30 -0
- package/dist/data/storage/removeDirectory.min.js +1 -0
- package/dist/data/storage/removeFile.d.ts +8 -0
- package/dist/data/storage/removeFile.js +31 -0
- package/dist/data/storage/removeFile.min.js +1 -0
- package/dist/data/storage/storagePath.d.ts +11 -0
- package/dist/data/storage/storagePath.js +25 -0
- package/dist/data/storage/storagePath.min.js +1 -0
- package/dist/data/storage/writeJson.d.ts +17 -0
- package/dist/data/storage/writeJson.js +37 -0
- package/dist/data/storage/writeJson.min.js +1 -0
- package/dist/data/storage/writeSet.d.ts +64 -0
- package/dist/data/storage/writeSet.js +306 -0
- package/dist/data/storage/writeSet.min.js +1 -0
- package/dist/data/utilities/compareAs.d.ts +1 -0
- package/dist/data/utilities/compareAs.js +2 -2
- package/dist/data/utilities/compareAs.min.js +1 -1
- package/dist/data/utilities/config.d.ts +37 -0
- package/dist/data/utilities/config.js +77 -5
- package/dist/data/utilities/config.min.js +1 -1
- package/dist/data/utilities/databaseLock.d.ts +7 -0
- package/dist/data/utilities/databaseLock.js +19 -4
- package/dist/data/utilities/databaseLock.min.js +1 -1
- package/dist/data/utilities/deregisterRecord.js +4 -10
- package/dist/data/utilities/deregisterRecord.min.js +1 -1
- package/dist/data/utilities/getConditionIndexedList.js +3 -4
- package/dist/data/utilities/getConditionIndexedList.min.js +1 -1
- package/dist/data/utilities/indexBuffer.d.ts +35 -0
- package/dist/data/utilities/indexBuffer.js +139 -0
- package/dist/data/utilities/indexBuffer.min.js +1 -0
- package/dist/data/utilities/indexHas.d.ts +13 -0
- package/dist/data/utilities/indexHas.js +48 -0
- package/dist/data/utilities/indexHas.min.js +1 -0
- package/dist/data/utilities/joinBases.d.ts +29 -0
- package/dist/data/utilities/joinBases.js +128 -0
- package/dist/data/utilities/joinBases.min.js +1 -0
- package/dist/data/utilities/joinKey.d.ts +17 -0
- package/dist/data/utilities/joinKey.js +31 -0
- package/dist/data/utilities/joinKey.min.js +1 -0
- package/dist/data/utilities/keyIndexFiles.d.ts +16 -0
- package/dist/data/utilities/keyIndexFiles.js +60 -0
- package/dist/data/utilities/keyIndexFiles.min.js +1 -0
- package/dist/data/utilities/knownFields.d.ts +26 -0
- package/dist/data/utilities/knownFields.js +128 -0
- package/dist/data/utilities/knownFields.min.js +1 -0
- package/dist/data/utilities/packedTable.d.ts +34 -0
- package/dist/data/utilities/packedTable.js +60 -0
- package/dist/data/utilities/packedTable.min.js +1 -0
- package/dist/data/utilities/parsers/findInsertValues.d.ts +3 -3
- package/dist/data/utilities/parsers/findInsertValues.js +28 -12
- package/dist/data/utilities/parsers/findInsertValues.min.js +1 -1
- package/dist/data/utilities/parsers/maskStrings.js +11 -2
- package/dist/data/utilities/parsers/maskStrings.min.js +1 -1
- package/dist/data/utilities/parsers/parseAlter.d.ts +33 -0
- package/dist/data/utilities/parsers/parseAlter.js +84 -0
- package/dist/data/utilities/parsers/parseAlter.min.js +1 -0
- package/dist/data/utilities/parsers/parseCreate.d.ts +14 -0
- package/dist/data/utilities/parsers/parseCreate.js +13 -5
- package/dist/data/utilities/parsers/parseCreate.min.js +1 -1
- package/dist/data/utilities/parsers/parseDatabaseStatement.d.ts +17 -0
- package/dist/data/utilities/parsers/parseDatabaseStatement.js +82 -0
- package/dist/data/utilities/parsers/parseDatabaseStatement.min.js +1 -0
- package/dist/data/utilities/parsers/parseQuery.d.ts +3 -0
- package/dist/data/utilities/parsers/parseQuery.js +24 -0
- package/dist/data/utilities/parsers/parseQuery.min.js +1 -1
- package/dist/data/utilities/parsers/startsQuery.js +1 -1
- package/dist/data/utilities/parsers/startsQuery.min.js +1 -1
- package/dist/data/utilities/recordExists.js +2 -7
- package/dist/data/utilities/recordExists.min.js +1 -1
- package/dist/data/utilities/registerRecord.js +4 -10
- package/dist/data/utilities/registerRecord.min.js +1 -1
- package/dist/data/utilities/removeFromIndex.js +4 -10
- package/dist/data/utilities/removeFromIndex.min.js +1 -1
- package/dist/data/utilities/retrieveFile.d.ts +5 -0
- package/dist/data/utilities/retrieveFile.js +7 -16
- package/dist/data/utilities/retrieveFile.min.js +1 -1
- package/dist/data/utilities/retrieveFiles.d.ts +15 -0
- package/dist/data/utilities/retrieveFiles.js +48 -0
- package/dist/data/utilities/retrieveFiles.min.js +1 -0
- package/dist/data/utilities/retrieveRecord.d.ts +6 -0
- package/dist/data/utilities/retrieveRecord.js +24 -1
- package/dist/data/utilities/retrieveRecord.min.js +1 -1
- package/dist/data/utilities/retrieveRecords.d.ts +22 -0
- package/dist/data/utilities/retrieveRecords.js +60 -10
- package/dist/data/utilities/retrieveRecords.min.js +1 -1
- package/dist/data/utilities/rowLookup.d.ts +38 -0
- package/dist/data/utilities/rowLookup.js +99 -0
- package/dist/data/utilities/rowLookup.min.js +1 -0
- package/dist/data/utilities/runQuery.js +28 -4
- package/dist/data/utilities/runQuery.min.js +1 -1
- package/dist/data/utilities/semiJoin.d.ts +27 -0
- package/dist/data/utilities/semiJoin.js +97 -0
- package/dist/data/utilities/semiJoin.min.js +1 -0
- package/dist/data/utilities/updateIndex.js +5 -14
- package/dist/data/utilities/updateIndex.min.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -0
- package/dist/index.min.js +1 -1
- package/package.json +10 -4
- package/dist/data/utilities/getJoinIndexedList.d.ts +0 -3
- package/dist/data/utilities/getJoinIndexedList.js +0 -76
- package/dist/data/utilities/getJoinIndexedList.min.js +0 -1
- package/dist/data/utilities/getMatchingIndexes.d.ts +0 -2
- package/dist/data/utilities/getMatchingIndexes.js +0 -28
- package/dist/data/utilities/getMatchingIndexes.min.js +0 -1
- package/dist/data/utilities/matchIndexedJoins.d.ts +0 -2
- package/dist/data/utilities/matchIndexedJoins.js +0 -63
- package/dist/data/utilities/matchIndexedJoins.min.js +0 -1
- package/dist/data/utilities/restrictIndexedList.d.ts +0 -2
- package/dist/data/utilities/restrictIndexedList.js +0 -45
- package/dist/data/utilities/restrictIndexedList.min.js +0 -1
- package/dist/data/utilities/useJoinClause.d.ts +0 -6
- package/dist/data/utilities/useJoinClause.js +0 -57
- package/dist/data/utilities/useJoinClause.min.js +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,93 @@
|
|
|
1
1
|
# json-fs-query
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A git-native JSON data store for static sites. Your data lives in a folder of plain JSON files, committed with the
|
|
4
|
+
rest of the site: readable, diffable and reviewed in pull requests like code. json-fs-query adds what a folder of
|
|
5
|
+
files doesn't have on its own:
|
|
6
|
+
|
|
7
|
+
- **A schema**: field types, required and nullable fields, defaults.
|
|
8
|
+
- **Keys enforced**: primary, unique and foreign keys, checked on every write (a delete that would leave a dangling
|
|
9
|
+
reference is refused).
|
|
10
|
+
- **A SQL-like query language**: `read workouts where date = '2024-06-02' sort reps desc`, with joins, `group by`,
|
|
11
|
+
`alter`, multi-row inserts, and statements that change nothing if any row fails.
|
|
12
|
+
- **Reads straight from static hosting**: in a browser it reads the files over `fetch()` (GitHub Pages, Netlify, any
|
|
13
|
+
CDN), so a site can query its data with no database server. Writes run in Node: a script or a build step.
|
|
14
|
+
|
|
15
|
+
## When to use it, and when not to
|
|
16
|
+
|
|
17
|
+
It suits data that's written by you (or your build) and read by your visitors: a portfolio's projects, a blog's
|
|
18
|
+
posts, a quiz bank, a workout log, a small catalogue. Small to medium size: hundreds to about ten thousand rows a
|
|
19
|
+
record. Data you want to see change in `git diff`, and ship without running anything server-side.
|
|
20
|
+
|
|
21
|
+
Use SQLite, or a real database server, instead when:
|
|
22
|
+
|
|
23
|
+
- **Visitors write data.** Writes need file system access, so a browser can only read. Something has to run a server
|
|
24
|
+
to accept writes.
|
|
25
|
+
- **Several programs write at once.** Calls are queued within one Node process; two processes writing one folder
|
|
26
|
+
aren't protected from each other.
|
|
27
|
+
- **The data is big.** Each row is a file, and a read loads the rows it needs into memory. The benchmarks go up to
|
|
28
|
+
10,000 rows; once the files stop being something you'd read in a diff, SQLite will be faster and smaller.
|
|
29
|
+
- **You need what isn't here yet**: transactions across several statements, aggregates (`count`, `sum`), outer joins,
|
|
30
|
+
date and decimal field types, multi-field foreign keys, and `cascade` / `set null` on delete (a referenced row can't
|
|
31
|
+
be deleted while rows point at it, or checks can be turned off with `configure()`). These are planned, not built.
|
|
32
|
+
|
|
33
|
+
## How fast
|
|
34
|
+
|
|
35
|
+
At 1,000 rows (a table of 100 parents and one of 1,000 children with a foreign key and an index), before the
|
|
36
|
+
performance work (3.1.4) and now. `BENCHMARKS.md` has every run and what each one measures.
|
|
37
|
+
|
|
38
|
+
| Operation, 1,000 rows | 3.1.4 | Now |
|
|
39
|
+
|---|---:|---:|
|
|
40
|
+
| Insert 1,000 rows, one `insert` each | 6.5 min | 5.7 s |
|
|
41
|
+
| Insert 1,000 rows in one `insert` | - | 0.43 s |
|
|
42
|
+
| Read every row | 376 ms | 151 ms |
|
|
43
|
+
| Indexed `where` | 7.8 ms | 4.2 ms |
|
|
44
|
+
| One join | 5.2 s | 176 ms |
|
|
45
|
+
| Two joins | 5.6 s | 177 ms |
|
|
46
|
+
| Update one row by key | 402 ms | 157 ms |
|
|
47
|
+
| Delete one row by key | 784 ms | 156 ms |
|
|
48
|
+
| Browser: read every row | 4.3 s | 689 ms, packed 11 ms |
|
|
49
|
+
| Browser: one join | 15.2 s | 732 ms, packed 20 ms |
|
|
50
|
+
| Browser: two joins | 26.1 s | 744 ms, packed 33 ms |
|
|
51
|
+
| Size of the data | 438 KB | 359 KB |
|
|
52
|
+
|
|
53
|
+
The browser rows are read from a local server; over a real network each request costs more, which is what packed
|
|
54
|
+
tables (see In the browser) are for: one request per record instead of one per row.
|
|
55
|
+
|
|
56
|
+
## A static site, end to end
|
|
57
|
+
|
|
58
|
+
A Node script owns the data: it creates the records once and adds rows whenever there are new ones.
|
|
5
59
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
60
|
+
```js
|
|
61
|
+
// scripts/add-posts.js
|
|
62
|
+
const { configure, query, packDatabase } = require('json-fs-query')
|
|
63
|
+
|
|
64
|
+
configure({ databasePath: 'site/database/' })
|
|
65
|
+
|
|
66
|
+
await query(`create if not exists authors (_id string auto, name string, primary key (_id));
|
|
67
|
+
create if not exists posts (_id string auto, author_id string, title string, published string,
|
|
68
|
+
primary key (_id), index (published), foreign key (author_id) references authors (_id))`)
|
|
69
|
+
|
|
70
|
+
const [[author]] = await query("insert authors values name = 'Joshua'")
|
|
71
|
+
await query(`insert posts values
|
|
72
|
+
(author_id = '${author._id}', title = 'First post', published = '2026-11-01'),
|
|
73
|
+
(author_id = '${author._id}', title = 'Second post', published = '2026-11-08')`)
|
|
74
|
+
|
|
75
|
+
await packDatabase() // one file per record, for the browser
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Commit `site/database/` and deploy `site/` as usual. The page reads it with no server of its own:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<script src="https://unpkg.com/json-fs-query"></script>
|
|
82
|
+
<script>
|
|
83
|
+
jsonFsQuery.configure({ databasePath: 'database/' })
|
|
84
|
+
jsonFsQuery.query("read posts merge posts.author_id with authors._id sort published desc limit 10")
|
|
85
|
+
.then(([posts]) => console.log(posts)) // each post with its author's name as "authors.name"
|
|
86
|
+
</script>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
New posts go in the same way (an `insert`, or a JSON file edited by hand), then `packDatabase()`, commit and deploy. A
|
|
90
|
+
test that calls `checkPacked()` (see Packed tables) catches a forgotten `packDatabase()`.
|
|
12
91
|
|
|
13
92
|
## Install
|
|
14
93
|
|
|
@@ -45,10 +124,17 @@ JSON can leave a key out or store it as `null`:
|
|
|
45
124
|
| Definition | Left out | `null` |
|
|
46
125
|
|---|---|---|
|
|
47
126
|
| `{ name: 'note', type: 'string' }` | error | error |
|
|
48
|
-
| `{ name: 'note', type: 'string', optional: true }` | allowed |
|
|
127
|
+
| `{ name: 'note', type: 'string', optional: true }` | allowed | same as left out |
|
|
49
128
|
| `{ name: 'note', type: 'string', nullable: true }` | error | stored as `null` |
|
|
50
129
|
| `{ name: 'note', type: 'string', optional: true, nullable: true }` | allowed | stored as `null` |
|
|
51
130
|
|
|
131
|
+
An optional field that isn't nullable can never hold `null`, so a `null` given for it means "no value": `insert ...
|
|
132
|
+
note = null` stores the row without `note` (a default fills it in, as for a field left out), and `update ... set
|
|
133
|
+
note = null` removes `note` from the rows it changes, which is how an optional field is cleared (not a field of the
|
|
134
|
+
primary key: rows are found by it). As in SQL, a default only fills in a field when a row is inserted, so clearing a
|
|
135
|
+
field with a default leaves it out rather than setting the default. Nullable fields store `null`, and required fields
|
|
136
|
+
that aren't nullable reject it.
|
|
137
|
+
|
|
52
138
|
`useDefault: true, defaultValue: …` fills in a field that is left out. `create()` checks the default against the
|
|
53
139
|
field's type (a `null` default needs `nullable: true`) and throws if it doesn't fit. Primary key fields can't be
|
|
54
140
|
nullable. Each key needs a non-empty `fields` list naming defined fields, and unknown key properties are rejected.
|
|
@@ -69,6 +155,29 @@ Keywords (`where`, `and`, `sort`, `limit`, `create`, `show records`...), command
|
|
|
69
155
|
`delete`), sort directions (`asc`, `desc`) and comparators can be written in any case: `READ t` and `sort a ASC` work.
|
|
70
156
|
Record and field names are case-sensitive.
|
|
71
157
|
|
|
158
|
+
Every record and field a query names must exist, as in SQL. A field means the queried record's own unless it's written
|
|
159
|
+
`<record>.<field>`, and it has to be one that record defines (it doesn't have to be stored in every row: an optional
|
|
160
|
+
field is fine). Otherwise the query fails before reading or writing anything, naming what's wrong and what's there:
|
|
161
|
+
`Record "workout" doesn't exist`, or `Unknown field "rep" in where: workouts has _id, exercise_id, date, reps`. In
|
|
162
|
+
`select`, the joined record's name stands for its rows, `<record>.<field>` picks the queried record's own field (named
|
|
163
|
+
as written: use `as` to rename it) or a merged record's, and a joined record's single field is an error, since its rows
|
|
164
|
+
are a list (merge it to put its fields on each row).
|
|
165
|
+
|
|
166
|
+
### Inserting several rows
|
|
167
|
+
|
|
168
|
+
One `insert` can add many rows: put each row's values in brackets, separated by commas.
|
|
169
|
+
|
|
170
|
+
```sql
|
|
171
|
+
insert workouts values (reps = 10, date = '2024-06-02'), (reps = 12, date = '2024-06-03'), (reps = 8, date = '2024-06-04')
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It resolves to the inserted rows, in order. Each row is checked against the rows already stored and the rows before it
|
|
175
|
+
in the statement (so two rows with the same primary or unique key fail, and a row's foreign key can point at a row
|
|
176
|
+
earlier in the same insert). The statement is all or nothing: if any row fails, none is written. Every index is read once
|
|
177
|
+
and the record file and each index are written once, so this is far faster than one `insert` per row: 1,000 rows take
|
|
178
|
+
about half a second, against about 7 seconds one at a time (see `BENCHMARKS.md`). One row without brackets
|
|
179
|
+
(`insert workouts values reps = 10, date = '2024-06-02'`) works as before.
|
|
180
|
+
|
|
72
181
|
### Strings
|
|
73
182
|
|
|
74
183
|
Strings use single or double quotes. A string closes only with the quote it opened with, so the other quote is just
|
|
@@ -380,6 +489,32 @@ it, that is an error. A record that another record's foreign key references can'
|
|
|
380
489
|
(unless `enforceForeignKeys` is turned off, see Configuration). `describe` resolves to `{ path, definition, keys }`
|
|
381
490
|
(the `describe()` function gives only the fields), and `show records` to an array of names.
|
|
382
491
|
|
|
492
|
+
### Changing a record's fields: `alter`
|
|
493
|
+
|
|
494
|
+
```sql
|
|
495
|
+
alter workouts add note string optional -- a new field (written as in create)
|
|
496
|
+
alter workouts add status string default 'planned' -- existing rows get the default
|
|
497
|
+
alter workouts drop note -- removed from the definition and every row
|
|
498
|
+
alter workouts rename date to day -- in every row, its keys and indexes, and foreign keys to it
|
|
499
|
+
alter workouts modify status string optional default 'planned' -- the field's whole new definition
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Each `alter` makes one change, all or nothing (several, separated by `;`, run in turn), and resolves to the record's
|
|
503
|
+
new fields. The `alter()` function takes the same changes as objects, e.g.
|
|
504
|
+
`alter('workouts', { action: 'add', field: { name: 'note', type: 'string', optional: true } })`.
|
|
505
|
+
|
|
506
|
+
- **add**: existing rows get the field's default, or a new id for an `auto` field. A required field with neither is
|
|
507
|
+
refused while the record has rows (they'd be missing it): give it a default, or make it optional.
|
|
508
|
+
- **drop**: refused while a key of the record uses the field, or another record's foreign key references it.
|
|
509
|
+
- **rename**: a key's index file named after the field moves to the new name; foreign keys that reference the field
|
|
510
|
+
(in any record) are renamed with it.
|
|
511
|
+
- **modify**: gives the field's whole new definition, as MySQL's `MODIFY` does: anything it leaves out is cleared, so
|
|
512
|
+
repeat a default to keep it (`modify status string optional` removes `default 'planned'`, as leaving out `optional`
|
|
513
|
+
makes the field required). The type, and whether the field is `auto`, can't change (that's for a later version).
|
|
514
|
+
Making it required fills rows that are missing it from the new default, and is refused without one; making it not
|
|
515
|
+
nullable removes the `null`s when it stays optional, and otherwise fills them from the default. When a key uses the
|
|
516
|
+
field, filled rows go through the same checks as an `update` (a unique key refuses the same default twice).
|
|
517
|
+
|
|
383
518
|
### Reading a query over several lines: `queryStatus()`
|
|
384
519
|
|
|
385
520
|
For a prompt which reads a query line by line (like the CLI), `queryStatus()` says whether the text so far is ready to
|
|
@@ -444,6 +579,18 @@ Queries that worked in 2.x can be errors now (each error names the problem), and
|
|
|
444
579
|
`Missing a whole number after limit: x`. In 2.x these were `Unrecognized query value: ...`. Check any code that
|
|
445
580
|
matches on that message.
|
|
446
581
|
|
|
582
|
+
### Upgrading from 3.2
|
|
583
|
+
|
|
584
|
+
- A field a record doesn't define is an error everywhere a query names one. In 3.2 `insert` dropped it without a word,
|
|
585
|
+
`update ... set` stored it outside the record's definition, and `where`, `sort`, `group by`, `select`, `on` and
|
|
586
|
+
`merge` quietly matched nothing (or left rows unsorted). A field that's only missing from some rows is fine, as long
|
|
587
|
+
as the record defines it.
|
|
588
|
+
- `select <record>.<field>` for the queried record picks its field; in 3.2 it gave nothing. For a joined record
|
|
589
|
+
without `merge`, it's an error saying to select the record (its rows) or merge it.
|
|
590
|
+
- A record that doesn't exist is `Record "<name>" doesn't exist` (in a browser, `... doesn't exist, or <name>.json
|
|
591
|
+
can't be read`), as `drop` and `describe` already said, instead of `Could not read <name>.json`. A join written
|
|
592
|
+
without the record to join (`read workouts on ...`) says so, instead of `Could not read .json`.
|
|
593
|
+
|
|
447
594
|
## Configuration
|
|
448
595
|
|
|
449
596
|
`configure()` sets where this package's data lives and how strictly it enforces foreign keys - call it once,
|
|
@@ -455,19 +602,78 @@ const { configure } = require('json-fs-query')
|
|
|
455
602
|
configure({
|
|
456
603
|
databasePath: 'database/', // where the JSON files live (default 'database/')
|
|
457
604
|
relativePath: '', // URL prefix prepended to databasePath when reading via fetch() in a browser
|
|
458
|
-
enforceForeignKeys: true
|
|
605
|
+
enforceForeignKeys: true, // RESTRICT delete/update of a record other entities still reference (default true)
|
|
606
|
+
readConcurrency: 16, // how many entry files a read has in flight at once (default 16)
|
|
607
|
+
format: 'compact', // how files are written: 'compact' or 'pretty' (default 'compact')
|
|
608
|
+
packedReads: undefined, // use packed files (see packDatabase); default true in a browser, false in Node
|
|
609
|
+
databasesPath: undefined, // a folder holding several databases (see Several databases)
|
|
610
|
+
database: undefined // the one of them in use
|
|
459
611
|
})
|
|
460
612
|
```
|
|
461
613
|
|
|
462
614
|
Turning `enforceForeignKeys` off restores the pre-1.0 behavior (no referential integrity checks at all) - useful if
|
|
463
615
|
it doesn't fit a particular workflow, mirroring MySQL's own toggleable `foreign_key_checks`.
|
|
464
616
|
|
|
617
|
+
Reads fetch several entry files at once, `readConcurrency` at a time, and keep the record's order. In a browser each
|
|
618
|
+
file is one request: 16 suits HTTP/2 static hosts (such as GitHub Pages), while over HTTP/1.1 a browser only opens about
|
|
619
|
+
6 connections per host anyway. `readConcurrency: 1` reads one file at a time, as every read did before this setting.
|
|
620
|
+
|
|
621
|
+
### Several databases: `databasesPath`, `use`
|
|
622
|
+
|
|
623
|
+
One program can keep several databases (say, real data and a test copy) in one folder and switch between them:
|
|
624
|
+
|
|
625
|
+
```js
|
|
626
|
+
configure({ databasesPath: 'databases/', database: 'real' }) // databases/real/ is the one in use
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```sql
|
|
630
|
+
create database copy; -- a new, empty databases/copy/ (it doesn't switch to it)
|
|
631
|
+
use copy; -- the rest of this query, and the calls after it, use databases/copy/
|
|
632
|
+
show databases; -- ['real', 'copy'], in the order they were created
|
|
633
|
+
drop database if exists copy; -- deletes it and everything in it (not the one in use: use another first)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`use` does what `configure({ database })` does, and both work in a browser (reading only), for any database listed in
|
|
637
|
+
`databases/__DATABASES.json`, the list `create database` and `drop database` keep (a browser can't list a folder).
|
|
638
|
+
With `databasesPath` set, calls on all its databases queue together (see Several calls at once), so a `use` part way
|
|
639
|
+
through a call is safe. A browser runs calls side by side instead, with one database in use for the page, so there
|
|
640
|
+
`use` refuses while another call is running: await it, or switch with `configure({ database })` between calls. A
|
|
641
|
+
database folder copied in by hand (to copy a database) isn't listed yet: `create database if not exists <name>` lists
|
|
642
|
+
it. Without `databasesPath`, `databasePath` names the one database, as before. A record can still be called
|
|
643
|
+
`database`: `create database (...)` and a bare `drop database` act on that record.
|
|
644
|
+
|
|
645
|
+
### File format: `format` and `formatDatabase()`
|
|
646
|
+
|
|
647
|
+
Files are written as compact JSON by default: about a fifth smaller than indented JSON, and an editor shows them
|
|
648
|
+
formatted anyway. `format: 'pretty'` writes them indented by 2 spaces instead, for reading data in diffs (for example in
|
|
649
|
+
pull requests). Either format is always read, so a database can hold both: one written before this setting keeps its
|
|
650
|
+
indented files until they change.
|
|
651
|
+
|
|
652
|
+
`formatDatabase()` rewrites a whole database in one format (the configured one by default), leaving files already in
|
|
653
|
+
it alone, and resolves to how many JSON files it found and rewrote:
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
const { configure, formatDatabase } = require('json-fs-query')
|
|
657
|
+
|
|
658
|
+
configure({ databasePath: 'database/', format: 'pretty' })
|
|
659
|
+
await formatDatabase() // { files: 1119, rewritten: 1119 }
|
|
660
|
+
await formatDatabase('compact') // back to compact, whatever the setting
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
It reads and checks every file before writing any: one that isn't valid JSON fails the call with its path and nothing
|
|
664
|
+
is changed. Node only, like every write.
|
|
665
|
+
|
|
666
|
+
**Upgrading a database kept in git:** before this setting every file was written indented, so after upgrading each
|
|
667
|
+
file turns compact the next time it changes, and diffs mix both. Either run `formatDatabase('compact')` once and commit
|
|
668
|
+
the result on its own, or set `format: 'pretty'` to keep writing indented files.
|
|
669
|
+
|
|
465
670
|
## Several calls at once
|
|
466
671
|
|
|
467
|
-
Calls to `query()`, `create()`, `drop()
|
|
468
|
-
the order they were made, so concurrent requests (for
|
|
469
|
-
changes. All the queries in one `query()` string run together,
|
|
470
|
-
process: two processes writing to the same folder (say a server
|
|
672
|
+
Calls to `query()`, `create()`, `drop()`, `describe()`, `formatDatabase()`, `packDatabase()` and `checkPacked()` on
|
|
673
|
+
one database (one `databasePath`) run one at a time, in the order they were made, so concurrent requests (for
|
|
674
|
+
example in a web server) can't overwrite each other's changes. All the queries in one `query()` string run together,
|
|
675
|
+
with no other call in between. This covers one Node process: two processes writing to the same folder (say a server
|
|
676
|
+
and a CLI) are not protected from each other.
|
|
471
677
|
|
|
472
678
|
`configure()` can't point `databasePath` at another folder while calls are still running or queued (it throws:
|
|
473
679
|
await them first), since each call reads the setting again at every step. The same folder written another way
|
|
@@ -491,6 +697,41 @@ Reads work in the browser via `fetch()` - `create`/`drop`, and any write through
|
|
|
491
697
|
`delete`), need real filesystem access and only work in Node. They're still present in the browser build (same
|
|
492
698
|
public API either way), but throw if actually called there, rather than being silently unavailable.
|
|
493
699
|
|
|
700
|
+
### Packed tables: one request per record
|
|
701
|
+
|
|
702
|
+
Read over `fetch()`, every entry file is one request, so reading 1,000 rows is 1,000 requests. `packDatabase()` writes
|
|
703
|
+
each record's rows into one file, `__packed/<record>.json`, and a browser then reads the whole record in one request:
|
|
704
|
+
at 1,000 rows a full read takes about 25 ms instead of 1 s (see `BENCHMARKS.md`). Run it in Node as a build step
|
|
705
|
+
whenever the data changes, and serve (or commit) the `__packed/` folder with the rest of the database:
|
|
706
|
+
|
|
707
|
+
```js
|
|
708
|
+
const { configure, packDatabase, checkPacked } = require('json-fs-query')
|
|
709
|
+
|
|
710
|
+
configure({ databasePath: 'database/' })
|
|
711
|
+
await packDatabase() // { records: 4, rows: 1452 }
|
|
712
|
+
await checkPacked() // { stale: [], orphaned: [] } - in a test, to catch a forgotten packDatabase()
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The entry files stay the copy that's edited and reviewed; a packed file is only ever a copy of them:
|
|
716
|
+
|
|
717
|
+
- Any `insert`, `update`, `delete` or `drop` through json-fs-query removes that record's packed file in the same
|
|
718
|
+
statement (a statement that changes no rows leaves it), so a browser never reads a packed file in place of a change.
|
|
719
|
+
- A packed file is only used while its entries match the record file, so rows added or removed without json-fs-query
|
|
720
|
+
fall back to the entry files.
|
|
721
|
+
- A row edited by hand keeps the same entries, so its packed file still looks current and a browser reads the old
|
|
722
|
+
row: run `packDatabase()` after hand edits, and run `checkPacked()` in CI (or a test) so a forgotten one fails the
|
|
723
|
+
build. It reports packed files that don't match their entry files (`stale`), and packed files of records that no
|
|
724
|
+
longer exist (`orphaned`, which `packDatabase()` removes).
|
|
725
|
+
|
|
726
|
+
A packed file is only read when a read needs more rows than one round of requests (`readConcurrency`, 16 by default).
|
|
727
|
+
A lookup of a few rows (an indexed `where` on a key, say) reads the index and those rows as before, so it never
|
|
728
|
+
downloads a whole record, and a record of 16 rows or fewer is never asked for a packed file. Indexes stay in their own
|
|
729
|
+
files. A larger record without a packed file costs one extra request that comes back as not found, before its entry
|
|
730
|
+
files are read as before.
|
|
731
|
+
|
|
732
|
+
Browsers use packed files by default and Node doesn't (it reads the entry files, which are always current). Change it
|
|
733
|
+
with `configure({ packedReads: true })` or `false`.
|
|
734
|
+
|
|
494
735
|
## Documentation
|
|
495
736
|
|
|
496
737
|
The reference for every function is in [`docs/`](https://joshuaheagle.com/projects/json-fs-query/docs/index.html) (or open `docs/index.html` locally). It is generated from the TypeScript source, and each module is a folder of `src/data/`:
|
|
@@ -499,10 +740,11 @@ The reference for every function is in [`docs/`](https://joshuaheagle.com/projec
|
|
|
499
740
|
| --- | --- |
|
|
500
741
|
| `introspect` | Create, drop and describe a JSON-file "table" (and the policies that validate one) |
|
|
501
742
|
| `queries` | Parse a query string and run it - reads, inserts, updates, deletes, joins, `where`, `sortBy`, `groupBy` |
|
|
743
|
+
| `storage` | Every file read, write, delete and listing (disk in Node, `fetch()` in a browser), the per-call cache and write set |
|
|
502
744
|
| `utilities` | Shared helpers the queries use - retrieving records/files/indexes and merging join results |
|
|
503
745
|
|
|
504
746
|
`src/index.ts` is the package's public entry point - it re-exports `query`, `quote`, `queryStatus`, `create`, `drop`,
|
|
505
|
-
`describe`, `configure`, and the schema-definition types (`fieldProperties`, `keyProperties`, etc.) needed to build a `create()` call. Everything else
|
|
747
|
+
`alter`, `describe`, `formatDatabase`, `packDatabase`, `checkPacked`, `configure`, and the schema-definition types (`fieldProperties`, `keyProperties`, etc.) needed to build a `create()` call. Everything else
|
|
506
748
|
under `src/data/` is an internal module, reachable but not part of the supported public API.
|
|
507
749
|
|
|
508
750
|
## Development
|