inibase 2.0.1 → 3.1.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 +211 -2
- package/dist/expression.d.ts +159 -0
- package/dist/expression.js +495 -0
- package/dist/file.d.ts +20 -1
- package/dist/file.js +211 -16
- package/dist/index.d.ts +177 -1
- package/dist/index.js +1414 -150
- package/dist/journal.d.ts +109 -0
- package/dist/journal.js +263 -0
- package/dist/utils.js +54 -0
- package/package.json +7 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-ahead journal for crash-atomic, multi-file commits.
|
|
3
|
+
*
|
|
4
|
+
* A transaction writes its intent to `journal.jsonl` (fsynced) before touching
|
|
5
|
+
* any live file, then swaps each live file aside (see `backup`), renames the
|
|
6
|
+
* freshly-written temp file into place, optionally renames the pagination
|
|
7
|
+
* metadata file, and finally appends a `commit` marker (fsynced).
|
|
8
|
+
*
|
|
9
|
+
* Two journal layouts share one recovery rule:
|
|
10
|
+
* - single-table ops: one `begin` entry carrying `files` + `pagination`;
|
|
11
|
+
* - database transactions: a `begin` entry (tables only) followed by one `op`
|
|
12
|
+
* entry per staged mutation (`files` + optional `pagination`), then `commit`.
|
|
13
|
+
*
|
|
14
|
+
* Recovery rule (run under the lock before any access):
|
|
15
|
+
* - journal without `commit` -> roll back (restore backups, undo pagination,
|
|
16
|
+
* discard temps);
|
|
17
|
+
* - journal with `commit` -> roll forward (complete swaps, discard backups).
|
|
18
|
+
*
|
|
19
|
+
* After recovery the journal is removed, so a logical operation spanning many
|
|
20
|
+
* column files + pagination (single- or multi-table) is atomic w.r.t. crashes.
|
|
21
|
+
*/
|
|
22
|
+
export interface JournalFileOp {
|
|
23
|
+
/** Absolute path of the live file being replaced or removed. */
|
|
24
|
+
live: string;
|
|
25
|
+
/** Absolute path where the original file is parked while the swap is in flight. */
|
|
26
|
+
backup: string;
|
|
27
|
+
/** Absolute path of the fully-written replacement, or null for pure removals. */
|
|
28
|
+
tmp: string | null;
|
|
29
|
+
/** Whether the live file existed when the transaction began. */
|
|
30
|
+
existed: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface JournalBeginEntry {
|
|
33
|
+
txn: string;
|
|
34
|
+
type: "begin";
|
|
35
|
+
/** Tables touched by a database transaction (single-table ops omit this). */
|
|
36
|
+
tables?: string[];
|
|
37
|
+
/** Single-table ops carry their intent directly on the begin entry. */
|
|
38
|
+
files?: JournalFileOp[];
|
|
39
|
+
/** Old/new absolute pagination file paths, or null when counts don't change. */
|
|
40
|
+
pagination?: {
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
} | null;
|
|
44
|
+
}
|
|
45
|
+
export interface JournalOpEntry {
|
|
46
|
+
txn: string;
|
|
47
|
+
type: "op";
|
|
48
|
+
files: JournalFileOp[];
|
|
49
|
+
/** Old/new absolute pagination file paths, or null when counts don't change. */
|
|
50
|
+
pagination?: {
|
|
51
|
+
from: string;
|
|
52
|
+
to: string;
|
|
53
|
+
} | null;
|
|
54
|
+
}
|
|
55
|
+
export interface JournalCommitEntry {
|
|
56
|
+
txn: string;
|
|
57
|
+
type: "commit";
|
|
58
|
+
}
|
|
59
|
+
export type JournalEntry = JournalBeginEntry | JournalOpEntry | JournalCommitEntry;
|
|
60
|
+
export declare class Journal {
|
|
61
|
+
readonly path: string;
|
|
62
|
+
private readonly txn;
|
|
63
|
+
private handle;
|
|
64
|
+
constructor(tablePath: string, txn: string);
|
|
65
|
+
private openJournal;
|
|
66
|
+
private append;
|
|
67
|
+
begin(files: JournalFileOp[], pagination?: JournalBeginEntry["pagination"]): Promise<void>;
|
|
68
|
+
commit(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Undo an in-flight transaction whose `commit` marker was never written.
|
|
71
|
+
* Restores originals from backups, undoes the pagination rename (if any),
|
|
72
|
+
* removes created files and leftovers, then discards the journal.
|
|
73
|
+
*/
|
|
74
|
+
rollback(): Promise<void>;
|
|
75
|
+
dispose(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Write-ahead journal for a database-level transaction spanning multiple
|
|
79
|
+
* tables. Holds `begin(tables)` once, then one `op` per staged mutation, and
|
|
80
|
+
* finally a single `commit` marker. Lives in `<database>/.tmp/journal.jsonl`
|
|
81
|
+
* next to the per-table `.tmp` directories.
|
|
82
|
+
*/
|
|
83
|
+
export declare class DatabaseJournal {
|
|
84
|
+
readonly path: string;
|
|
85
|
+
private readonly txn;
|
|
86
|
+
private handle;
|
|
87
|
+
constructor(dbPath: string, txn: string);
|
|
88
|
+
private openJournal;
|
|
89
|
+
private append;
|
|
90
|
+
begin(tables: string[]): Promise<void>;
|
|
91
|
+
op(files: JournalFileOp[], pagination?: JournalOpEntry["pagination"]): Promise<void>;
|
|
92
|
+
commit(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Undo the transaction: restore any published files (crash mid-commit in
|
|
95
|
+
* the same process — callers normally roll back before anything is
|
|
96
|
+
* published), discard temps/journals. Handles the un-published case
|
|
97
|
+
* identically (no backups exist -> restores are no-ops).
|
|
98
|
+
*/
|
|
99
|
+
rollback(): Promise<void>;
|
|
100
|
+
dispose(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Recover from a crash. `tmpDirPath` is a `.tmp` directory (a table's or the
|
|
104
|
+
* database's) where `journal.jsonl` lives, in either single-table or
|
|
105
|
+
* database-transaction layout. Entries carry absolute paths, so no other
|
|
106
|
+
* context is needed. Must be called while holding the corresponding lock so no
|
|
107
|
+
* live writer can interleave.
|
|
108
|
+
*/
|
|
109
|
+
export declare function recover(tmpDirPath: string): Promise<void>;
|
package/dist/journal.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { open, readFile, rename, rmdir, unlink, } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
// Durability mirror of src/file.ts (kept local to avoid an ESM import cycle:
|
|
4
|
+
// file.ts imports recover() from here). `none` skips fsyncs, `full` fsyncs
|
|
5
|
+
// journal appends, recoveries and directory touches.
|
|
6
|
+
const DURABLE = (process.env.INIBASE_DURABILITY ?? "full") !== "none";
|
|
7
|
+
const maybeSync = async (handle) => {
|
|
8
|
+
if (DURABLE)
|
|
9
|
+
await handle.sync();
|
|
10
|
+
};
|
|
11
|
+
const exists = async (path) => {
|
|
12
|
+
try {
|
|
13
|
+
const handle = await open(path, "r");
|
|
14
|
+
await handle.close();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const syncDir = async (path) => {
|
|
22
|
+
let handle = null;
|
|
23
|
+
try {
|
|
24
|
+
handle = await open(path, "r");
|
|
25
|
+
await maybeSync(handle);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Directory fsync is not supported on every platform/filesystem.
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await handle?.close();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Parse every journal entry; partial trailing lines (crash mid-append) are ignored. */
|
|
35
|
+
const readEntries = async (journalPath) => {
|
|
36
|
+
let content;
|
|
37
|
+
try {
|
|
38
|
+
content = await readFile(journalPath, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const line of content.split("\n")) {
|
|
45
|
+
if (!line)
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
entries.push(JSON.parse(line));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Partial trailing line: the append that wrote it was never
|
|
52
|
+
// fsynced, so the transaction never reached its `commit` marker.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return entries;
|
|
56
|
+
};
|
|
57
|
+
const readBegin = async (journalPath) => {
|
|
58
|
+
for (const entry of await readEntries(journalPath))
|
|
59
|
+
if (entry.type === "begin")
|
|
60
|
+
return entry;
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
/** Flatten begin/op entries into ordered file ops and pagination renames. */
|
|
64
|
+
const collectOps = (entries) => {
|
|
65
|
+
const ops = [];
|
|
66
|
+
const paginations = [];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.type === "commit")
|
|
69
|
+
continue;
|
|
70
|
+
if (entry.files?.length)
|
|
71
|
+
ops.push(...entry.files);
|
|
72
|
+
if (entry.pagination)
|
|
73
|
+
paginations.push(entry.pagination);
|
|
74
|
+
}
|
|
75
|
+
return { ops, paginations };
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Undo an uncommitted transaction (in reverse order so chained pagination
|
|
79
|
+
* renames and repeated swaps of the same live file unwind correctly), discard
|
|
80
|
+
* temps and backups, and fsync the affected directories.
|
|
81
|
+
*/
|
|
82
|
+
const applyRollback = async (ops, paginations) => {
|
|
83
|
+
for (const op of ops.toReversed()) {
|
|
84
|
+
if (op.existed && (await exists(op.backup))) {
|
|
85
|
+
await unlink(op.live).catch(() => { });
|
|
86
|
+
await rename(op.backup, op.live);
|
|
87
|
+
}
|
|
88
|
+
else if (!op.existed) {
|
|
89
|
+
await unlink(op.live).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
if (op.tmp)
|
|
92
|
+
await unlink(op.tmp).catch(() => { });
|
|
93
|
+
await unlink(op.backup).catch(() => { });
|
|
94
|
+
// Drop the per-txn backup subdirectory once it is empty.
|
|
95
|
+
await rmEmptyDir(dirname(op.backup));
|
|
96
|
+
}
|
|
97
|
+
for (const { from, to } of paginations.toReversed())
|
|
98
|
+
if ((await exists(to)) && !(await exists(from)))
|
|
99
|
+
await rename(to, from);
|
|
100
|
+
};
|
|
101
|
+
/** Complete a committed transaction: finish any missing swaps, drop backups. */
|
|
102
|
+
const applyRollForward = async (ops, paginations) => {
|
|
103
|
+
for (const op of ops) {
|
|
104
|
+
if (op.tmp && !(await exists(op.live)) && (await exists(op.tmp)))
|
|
105
|
+
await rename(op.tmp, op.live);
|
|
106
|
+
await unlink(op.backup).catch(() => { });
|
|
107
|
+
if (op.tmp)
|
|
108
|
+
await unlink(op.tmp).catch(() => { });
|
|
109
|
+
await rmEmptyDir(dirname(op.backup));
|
|
110
|
+
}
|
|
111
|
+
for (const { from, to } of paginations)
|
|
112
|
+
if ((await exists(from)) && !(await exists(to)))
|
|
113
|
+
await rename(from, to);
|
|
114
|
+
};
|
|
115
|
+
/** Best-effort removal of an empty directory (per-txn backup dir). */
|
|
116
|
+
const rmEmptyDir = async (dirPath) => {
|
|
117
|
+
try {
|
|
118
|
+
await rmdir(dirPath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Not empty or already gone: leave it.
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
export class Journal {
|
|
125
|
+
path;
|
|
126
|
+
txn;
|
|
127
|
+
handle = null;
|
|
128
|
+
constructor(tablePath, txn) {
|
|
129
|
+
this.txn = txn;
|
|
130
|
+
this.path = join(tablePath, ".tmp", "journal.jsonl");
|
|
131
|
+
}
|
|
132
|
+
async openJournal() {
|
|
133
|
+
if (!this.handle)
|
|
134
|
+
this.handle = await open(this.path, "a+");
|
|
135
|
+
}
|
|
136
|
+
async append(entry) {
|
|
137
|
+
await this.openJournal();
|
|
138
|
+
const handle = this.handle;
|
|
139
|
+
if (!handle)
|
|
140
|
+
throw new Error("JOURNAL_NOT_OPEN");
|
|
141
|
+
await handle.writeFile(`${JSON.stringify(entry)}\n`);
|
|
142
|
+
await maybeSync(handle);
|
|
143
|
+
}
|
|
144
|
+
async begin(files, pagination = null) {
|
|
145
|
+
await this.append({ txn: this.txn, type: "begin", files, pagination });
|
|
146
|
+
}
|
|
147
|
+
async commit() {
|
|
148
|
+
await this.append({ txn: this.txn, type: "commit" });
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Undo an in-flight transaction whose `commit` marker was never written.
|
|
152
|
+
* Restores originals from backups, undoes the pagination rename (if any),
|
|
153
|
+
* removes created files and leftovers, then discards the journal.
|
|
154
|
+
*/
|
|
155
|
+
async rollback() {
|
|
156
|
+
try {
|
|
157
|
+
const begin = await readBegin(this.path);
|
|
158
|
+
if (!begin)
|
|
159
|
+
return;
|
|
160
|
+
const { ops, paginations } = collectOps([begin]);
|
|
161
|
+
await applyRollback(ops, paginations);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await this.dispose();
|
|
165
|
+
await unlink(this.path).catch(() => { });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async dispose() {
|
|
169
|
+
await this.handle?.close();
|
|
170
|
+
this.handle = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Write-ahead journal for a database-level transaction spanning multiple
|
|
175
|
+
* tables. Holds `begin(tables)` once, then one `op` per staged mutation, and
|
|
176
|
+
* finally a single `commit` marker. Lives in `<database>/.tmp/journal.jsonl`
|
|
177
|
+
* next to the per-table `.tmp` directories.
|
|
178
|
+
*/
|
|
179
|
+
export class DatabaseJournal {
|
|
180
|
+
path;
|
|
181
|
+
txn;
|
|
182
|
+
handle = null;
|
|
183
|
+
constructor(dbPath, txn) {
|
|
184
|
+
this.txn = txn;
|
|
185
|
+
this.path = join(dbPath, ".tmp", "journal.jsonl");
|
|
186
|
+
}
|
|
187
|
+
async openJournal() {
|
|
188
|
+
if (!this.handle)
|
|
189
|
+
this.handle = await open(this.path, "a+");
|
|
190
|
+
}
|
|
191
|
+
async append(entry) {
|
|
192
|
+
await this.openJournal();
|
|
193
|
+
const handle = this.handle;
|
|
194
|
+
if (!handle)
|
|
195
|
+
throw new Error("JOURNAL_NOT_OPEN");
|
|
196
|
+
await handle.writeFile(`${JSON.stringify(entry)}\n`);
|
|
197
|
+
await maybeSync(handle);
|
|
198
|
+
}
|
|
199
|
+
async begin(tables) {
|
|
200
|
+
await this.append({ txn: this.txn, type: "begin", tables });
|
|
201
|
+
}
|
|
202
|
+
async op(files, pagination = null) {
|
|
203
|
+
await this.append({ txn: this.txn, type: "op", files, pagination });
|
|
204
|
+
}
|
|
205
|
+
async commit() {
|
|
206
|
+
await this.append({ txn: this.txn, type: "commit" });
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Undo the transaction: restore any published files (crash mid-commit in
|
|
210
|
+
* the same process — callers normally roll back before anything is
|
|
211
|
+
* published), discard temps/journals. Handles the un-published case
|
|
212
|
+
* identically (no backups exist -> restores are no-ops).
|
|
213
|
+
*/
|
|
214
|
+
async rollback() {
|
|
215
|
+
try {
|
|
216
|
+
const { ops, paginations } = collectOps(await readEntries(this.path));
|
|
217
|
+
await applyRollback(ops, paginations);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
await this.dispose();
|
|
221
|
+
await unlink(this.path).catch(() => { });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async dispose() {
|
|
225
|
+
await this.handle?.close();
|
|
226
|
+
this.handle = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Recover from a crash. `tmpDirPath` is a `.tmp` directory (a table's or the
|
|
231
|
+
* database's) where `journal.jsonl` lives, in either single-table or
|
|
232
|
+
* database-transaction layout. Entries carry absolute paths, so no other
|
|
233
|
+
* context is needed. Must be called while holding the corresponding lock so no
|
|
234
|
+
* live writer can interleave.
|
|
235
|
+
*/
|
|
236
|
+
export async function recover(tmpDirPath) {
|
|
237
|
+
const journalPath = join(tmpDirPath, "journal.jsonl");
|
|
238
|
+
const entries = await readEntries(journalPath);
|
|
239
|
+
if (!entries.length)
|
|
240
|
+
return; // no journal -> nothing to recover
|
|
241
|
+
const begin = entries.find((entry) => entry.type === "begin");
|
|
242
|
+
if (!begin) {
|
|
243
|
+
// Journal exists but its begin entry never became durable: no rename
|
|
244
|
+
// was performed, so the live files are untouched. Discard the journal.
|
|
245
|
+
await unlink(journalPath).catch(() => { });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const committed = entries.some((entry) => entry.type === "commit");
|
|
249
|
+
const { ops, paginations } = collectOps(entries);
|
|
250
|
+
if (committed) {
|
|
251
|
+
// Roll forward: every rename already happened before `commit` was
|
|
252
|
+
// written; just make sure swaps completed and discard the backups.
|
|
253
|
+
await applyRollForward(ops, paginations);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Roll back: restore originals, undo pagination renames, remove files
|
|
257
|
+
// the transaction created and discard every leftover.
|
|
258
|
+
await applyRollback(ops, paginations);
|
|
259
|
+
}
|
|
260
|
+
await unlink(journalPath).catch(() => { });
|
|
261
|
+
await syncDir(tmpDirPath);
|
|
262
|
+
await syncDir(dirname(tmpDirPath));
|
|
263
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMPUTED_EXPR_MAX_LENGTH } from "./expression.js";
|
|
1
2
|
/**
|
|
2
3
|
* Type guard function to check if the input is an array of objects.
|
|
3
4
|
*
|
|
@@ -663,6 +664,15 @@ export const ERROR_MESSAGES = {
|
|
|
663
664
|
INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
|
|
664
665
|
INVALID_NAME: "Name {variable} is not valid",
|
|
665
666
|
NO_ENV: "No environment file found",
|
|
667
|
+
COMPUTED_FIELD_SYNTAX: "Invalid computed expression syntax in field {variable}",
|
|
668
|
+
COMPUTED_FIELD_UNKNOWN_FIELD: "Field {variable} references unknown field id {variable}",
|
|
669
|
+
COMPUTED_FIELD_INVALID_LINK: "Field {variable} has an invalid link at hop {variable}",
|
|
670
|
+
COMPUTED_FIELD_INVALID_TARGET: "Field {variable} references an invalid target ({variable})",
|
|
671
|
+
COMPUTED_FIELD_CONFLICT: "Field {variable} cannot be computed and required/unique/regex at the same time",
|
|
672
|
+
COMPUTED_FIELD_CYCLE: "Computed fields form a cycle: {variable}",
|
|
673
|
+
COMPUTED_FIELD_SETTABLE: "Field {variable} is computed and cannot be set directly",
|
|
674
|
+
COMPUTED_FIELD_DANGLING_LINK: "Computed field {variable} references a missing row in table {variable}",
|
|
675
|
+
COMPUTED_FIELD_ARITHMETIC: "Computed field {variable} failed evaluation ({variable})",
|
|
666
676
|
},
|
|
667
677
|
ar: {
|
|
668
678
|
TABLE_EMPTY: "الجدول {variable} فارغ",
|
|
@@ -678,6 +688,15 @@ export const ERROR_MESSAGES = {
|
|
|
678
688
|
INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
|
|
679
689
|
INVALID_NAME: "الاسم {variable} غير صالح",
|
|
680
690
|
NO_ENV: "لم يتم العثور على ملف البيئة",
|
|
691
|
+
COMPUTED_FIELD_SYNTAX: "صيغة الحقل المحسوب غير صالحة في الحقل {variable}",
|
|
692
|
+
COMPUTED_FIELD_UNKNOWN_FIELD: "الحقل {variable} يشير إلى معرف حقل غير معروف {variable}",
|
|
693
|
+
COMPUTED_FIELD_INVALID_LINK: "الحقل {variable} يحتوي على رابط غير صالح عند الخطوة {variable}",
|
|
694
|
+
COMPUTED_FIELD_INVALID_TARGET: "الحقل {variable} يشير إلى هدف غير صالح ({variable})",
|
|
695
|
+
COMPUTED_FIELD_CONFLICT: "لا يمكن أن يكون الحقل {variable} محسوبًا ومطلوبًا/فريدًا/بنمط في نفس الوقت",
|
|
696
|
+
COMPUTED_FIELD_CYCLE: "الحقول المحسوبة تشكل دورة: {variable}",
|
|
697
|
+
COMPUTED_FIELD_SETTABLE: "الحقل {variable} محسوب ولا يمكن تعيينه مباشرة",
|
|
698
|
+
COMPUTED_FIELD_DANGLING_LINK: "الحقل المحسوب {variable} يشير إلى صف غير موجود في الجدول {variable}",
|
|
699
|
+
COMPUTED_FIELD_ARITHMETIC: "فشل تقييم الحقل المحسوب {variable} ({variable})",
|
|
681
700
|
},
|
|
682
701
|
fr: {
|
|
683
702
|
TABLE_EMPTY: "La table {variable} est vide",
|
|
@@ -693,6 +712,15 @@ export const ERROR_MESSAGES = {
|
|
|
693
712
|
INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
|
|
694
713
|
INVALID_NAME: "Le nom {variable} n'est pas valide",
|
|
695
714
|
NO_ENV: "Aucun fichier d'environnement trouvé",
|
|
715
|
+
COMPUTED_FIELD_SYNTAX: "Syntaxe d'expression calculée invalide pour le champ {variable}",
|
|
716
|
+
COMPUTED_FIELD_UNKNOWN_FIELD: "Le champ {variable} référence un identifiant de champ inconnu {variable}",
|
|
717
|
+
COMPUTED_FIELD_INVALID_LINK: "Le champ {variable} possède un lien invalide à l'étape {variable}",
|
|
718
|
+
COMPUTED_FIELD_INVALID_TARGET: "Le champ {variable} référence une cible invalide ({variable})",
|
|
719
|
+
COMPUTED_FIELD_CONFLICT: "Le champ {variable} ne peut pas être calculé et requis/unique/regex à la fois",
|
|
720
|
+
COMPUTED_FIELD_CYCLE: "Les champs calculés forment un cycle : {variable}",
|
|
721
|
+
COMPUTED_FIELD_SETTABLE: "Le champ {variable} est calculé et ne peut pas être défini directement",
|
|
722
|
+
COMPUTED_FIELD_DANGLING_LINK: "Le champ calculé {variable} référence une ligne manquante dans la table {variable}",
|
|
723
|
+
COMPUTED_FIELD_ARITHMETIC: "L'évaluation du champ calculé {variable} a échoué ({variable})",
|
|
696
724
|
},
|
|
697
725
|
es: {
|
|
698
726
|
TABLE_EMPTY: "La tabla {variable} está vacía",
|
|
@@ -708,6 +736,15 @@ export const ERROR_MESSAGES = {
|
|
|
708
736
|
INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
|
|
709
737
|
INVALID_NAME: "El nombre {variable} no es válido",
|
|
710
738
|
NO_ENV: "No se encontró el archivo de entorno",
|
|
739
|
+
COMPUTED_FIELD_SYNTAX: "Sintaxis de expresión calculada no válida en el campo {variable}",
|
|
740
|
+
COMPUTED_FIELD_UNKNOWN_FIELD: "El campo {variable} referencia un id de campo desconocido {variable}",
|
|
741
|
+
COMPUTED_FIELD_INVALID_LINK: "El campo {variable} tiene un enlace no válido en el paso {variable}",
|
|
742
|
+
COMPUTED_FIELD_INVALID_TARGET: "El campo {variable} referencia un destino no válido ({variable})",
|
|
743
|
+
COMPUTED_FIELD_CONFLICT: "El campo {variable} no puede ser calculado y requerido/único/regex a la vez",
|
|
744
|
+
COMPUTED_FIELD_CYCLE: "Los campos calculados forman un ciclo: {variable}",
|
|
745
|
+
COMPUTED_FIELD_SETTABLE: "El campo {variable} es calculado y no puede establecerse directamente",
|
|
746
|
+
COMPUTED_FIELD_DANGLING_LINK: "El campo calculado {variable} referencia una fila inexistente en la tabla {variable}",
|
|
747
|
+
COMPUTED_FIELD_ARITHMETIC: "Falló la evaluación del campo calculado {variable} ({variable})",
|
|
711
748
|
},
|
|
712
749
|
};
|
|
713
750
|
/**
|
|
@@ -784,6 +821,23 @@ export const validateSchema = (schema, language = "en") => {
|
|
|
784
821
|
validateName(field.key, language);
|
|
785
822
|
if (field.table)
|
|
786
823
|
validateName(field.table, language);
|
|
824
|
+
if (typeof field.computed !== "undefined") {
|
|
825
|
+
const computed = field.computed;
|
|
826
|
+
const isValidForm = typeof computed === "string"
|
|
827
|
+
? computed.length > 0 && computed.length <= COMPUTED_EXPR_MAX_LENGTH
|
|
828
|
+
: typeof computed === "object" &&
|
|
829
|
+
computed !== null &&
|
|
830
|
+
typeof computed.expr === "string" &&
|
|
831
|
+
typeof computed.ast === "object" &&
|
|
832
|
+
computed.expr.length > 0 &&
|
|
833
|
+
computed.expr.length <= COMPUTED_EXPR_MAX_LENGTH;
|
|
834
|
+
if (!isValidForm)
|
|
835
|
+
throw createError(language, "COMPUTED_FIELD_SYNTAX", field.key);
|
|
836
|
+
if (field.required !== undefined ||
|
|
837
|
+
field.unique !== undefined ||
|
|
838
|
+
field.regex !== undefined)
|
|
839
|
+
throw createError(language, "COMPUTED_FIELD_CONFLICT", field.key);
|
|
840
|
+
}
|
|
787
841
|
if (field.children && isArrayOfObjects(field.children))
|
|
788
842
|
validateSchema(field.children, language);
|
|
789
843
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inibase",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Karim Amahtil",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/inicontent/inibase/issues"
|
|
19
19
|
},
|
|
20
|
-
"description": "A file-based & memory-efficient, serverless
|
|
20
|
+
"description": "A file-based & memory-efficient, serverless relational database with crash-atomic ACID: single-table DML + multi-table transactions (per-table writer locks, write-ahead journal + crash recovery, fsync-backed durability, begin/commit/rollback)",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22"
|
|
23
23
|
},
|
|
@@ -86,8 +86,13 @@
|
|
|
86
86
|
"prebuild": "biome check --write src",
|
|
87
87
|
"build": "tsc",
|
|
88
88
|
"benchmark": "./benchmark/run.js",
|
|
89
|
+
"benchmark:durability": "tsx ./benchmark/durability.ts",
|
|
90
|
+
"benchmark:computed": "tsx ./benchmark/computed.ts",
|
|
89
91
|
"test": "tsx ./tests/inibase.test.ts",
|
|
92
|
+
"test:durability": "tsx ./tests/durability.test.ts",
|
|
93
|
+
"test:transaction": "tsx ./tests/transaction.test.ts",
|
|
90
94
|
"test:utils": "tsx ./tests/utils.test.ts",
|
|
95
|
+
"test:expression": "tsx ./tests/expression.test.ts",
|
|
91
96
|
"test:advanced": "tsx ./tests/inibase.advanced.test.ts"
|
|
92
97
|
}
|
|
93
98
|
}
|