nodejs-store 2.0.0 → 2.0.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.
package/llms-full.txt ADDED
@@ -0,0 +1,506 @@
1
+ # nodejs-store — full documentation for LLMs
2
+
3
+ > Source of truth: https://github.com/coenddt/nodejs-store
4
+ > npm: `nodejs-store` | Node.js 18+ | MIT
5
+ > Backends: MongoDB, MySQL, SQLite, PostgreSQL
6
+
7
+ This file is a single-file, plain-text snapshot of the nodejs-store documentation, intended
8
+ for direct ingestion by language models. It mirrors README.md plus the FAQ and use-case
9
+ summaries.
10
+
11
+ ---
12
+
13
+ ## 1. What nodejs-store is
14
+
15
+ nodejs-store is a lightweight, backend-agnostic **data layer** for Node.js (not a full ORM).
16
+
17
+ You describe each model once as **pure JSON** — `fields`, `relations`, `computes`, `indexes`,
18
+ `read` / `write` role whitelists. From that description the library derives:
19
+
20
+ - **command planning**: GQL → MongoDB command JSON (`find` / `aggregate` / `countDocuments` / …),
21
+ - **dialect translation**: command JSON → parameterized SQL for MySQL / PostgreSQL / SQLite,
22
+ - **permission checks**: schema-level and field-level read/write, owner-condition injection,
23
+ - **computed columns**: `fn` (sync), `asyncFn` (async), `agg` (relation aggregation),
24
+ - **soft-delete archives**: every schema auto-registers a `<Model>Deleted` collection/table,
25
+ - **result rehydration**: flat JOIN rows → nested documents.
26
+
27
+ MongoDB is the **primary dialect**: queries are written in a MongoDB-flavoured GQL, and the
28
+ three relational backends adapt to it. That is what makes one schema portable across a
29
+ document store and three relational stores.
30
+
31
+ ### Architecture: three repositories
32
+
33
+ - `rust-store` — the shared **Rust core engine**. Implements GQL parsing, permission engine,
34
+ computed columns, command planning and SQL dialect translation. Pure logic, **no IO, no
35
+ clock, no random source** (`now` / `newId` are passed in by the host).
36
+ - `rust-store-node` (npm) — napi-rs binding exposing the core's command contract to JS hosts.
37
+ - `nodejs-store` (npm) — **this package**: the Node.js host. Owns driver IO, callbacks and
38
+ placeholder substitution.
39
+ - `py-store` (pip `storepy`, import `py_store`) — the Python asyncio twin of nodejs-store.
40
+
41
+ Same schemas, same GQL, same semantics across Node and Python.
42
+
43
+ ---
44
+
45
+ ## 2. Installation
46
+
47
+ ```bash
48
+ npm install nodejs-store
49
+ ```
50
+
51
+ Requires Node.js 18+ and one supported backend (MongoDB / MySQL / SQLite / PostgreSQL).
52
+
53
+ ---
54
+
55
+ ## 3. Quick start
56
+
57
+ ```js
58
+ const { MongoClient } = require('mongodb');
59
+ const { init, store } = require('nodejs-store');
60
+
61
+ const client = new MongoClient('mongodb://localhost:27017');
62
+ await client.connect();
63
+ await init(client.db('mydb')); // idempotently creates indexes for registered schemas
64
+
65
+ store.register({
66
+ name: 'Post',
67
+ collection: 'posts', // optional, defaults to name
68
+ idPrefix: 'PT', // string _id: prefix + base36 timestamp + random
69
+ fields: {
70
+ title: { type: 'string', default: '' },
71
+ status: { type: 'string', default: 'draft' },
72
+ tags: { type: 'array', default: [] },
73
+ },
74
+ computes: {
75
+ statusLabel: { type: 'string', depends: ['status'], fn: (doc) => (doc.status || '').toUpperCase() },
76
+ },
77
+ indexes: [{ keys: { status: 1, createdAt: -1 } }],
78
+ });
79
+
80
+ const doc = await store.insert('Post', { title: 'Hello' });
81
+
82
+ const items = await store.query(
83
+ 'Post($condition:@c0,$sort:@s1,$limit:@l) { title, status, statusLabel }',
84
+ { c0: { status: 'draft' }, s1: { createdAt: -1 }, l: 20 },
85
+ );
86
+ ```
87
+
88
+ The same schema and query run against PostgreSQL — only the datasource changes:
89
+
90
+ ```js
91
+ await init({ default: { kind: 'postgres', exec } });
92
+ const items = await store.query('Post($condition:@c0) { title, status }', { c0: { status: 'draft' } });
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 4. Supported backends
98
+
99
+ | Backend | Notes |
100
+ | --- | --- |
101
+ | MongoDB | native aggregation pipeline (`find` / `aggregate` / `$lookup`) |
102
+ | MySQL | parameterized SQL, `information_schema` introspection |
103
+ | SQLite | parameterized SQL, `sqlite_master` + `PRAGMA` introspection. **Sync driver** (`better-sqlite3`) — calls block the event loop by design; for high-concurrency hot paths prefer MySQL/PostgreSQL/MongoDB or isolate SQLite in its own process |
104
+ | PostgreSQL | parameterized SQL (`$n`), `RETURNING` support |
105
+
106
+ GQL tree queries compile to a single native query per backend.
107
+
108
+ ---
109
+
110
+ ## 5. GQL tree syntax
111
+
112
+ ```text
113
+ Model($condition:@c0,$sort:@s1,$skip:@sk,$limit:@l1) {
114
+ field1, field2, obj.subField,
115
+ Relation($condition:@c2,$sort:@s3,$limit:@l2) { f3, Nested { f4 } }
116
+ }
117
+ ```
118
+
119
+ - Parameter values are referenced from the params object by `@key`: `{ c0: {...}, s1: {...} }`.
120
+ - Object sub-fields use dot notation.
121
+ - Relations are declared in the schema (`type: 'many' | 'one'`) and resolved automatically —
122
+ never hand-write `$lookup`.
123
+ - `many` relations return arrays (`[]` when empty); `one` relations merge into the parent
124
+ document (`null` when missing).
125
+ - Relation-level `$sort` / `$skip` / `$limit` are **per-parent top-N** (each parent gets its
126
+ own window; translated to a SQL window function).
127
+
128
+ Fixed execution order for grouping queries:
129
+ `$condition` (WHERE) → `$group` (GROUP BY) → `$having` (HAVING) → `$sort` → `$skip`/`$limit` → projection.
130
+
131
+ > Removing an escape hatch: `$pipeline` passthrough and `store.aggregate()` were removed.
132
+ > A GQL containing `$pipeline` fails explicitly instead of being silently ignored.
133
+
134
+ ---
135
+
136
+ ## 6. Aggregation
137
+
138
+ Normalized aggregation lives **inside GQL** — there is no separate aggregate API.
139
+
140
+ ### 6.1 Root-level `$group` / `$having`
141
+
142
+ ```js
143
+ const rows = await store.query(
144
+ 'Course($condition:@c0,$group:@g0,$having:@h0,$sort:@s0,$limit:@l0){ status, n, total }',
145
+ {
146
+ c0: { status: { $ne: 'deleted' } },
147
+ g0: { by: ['status'], agg: { n: { $count: '*' }, total: { $sum: 'price' } } },
148
+ h0: { n: { $gt: 1 } },
149
+ s0: { total: -1 },
150
+ l0: 20,
151
+ },
152
+ );
153
+ ```
154
+
155
+ - Whitelisted operators: `$count` / `$sum` / `$avg` / `$min` / `$max`.
156
+ - `$count` accepts `'*'` (row count) or a scalar field name (non-null count).
157
+ - Omit `by` (or pass `[]`) for a single all-table group; empty input still returns one row
158
+ (`$count` → `0`, others → `null`).
159
+ - Sorting / pagination apply to the **grouped result**.
160
+ - `$having` must be used with `$group`.
161
+
162
+ ### 6.2 Relation aggregate predicates (semi-join / anti-join)
163
+
164
+ Filter parents by an aggregate over a relation, **without fanning out**:
165
+
166
+ ```js
167
+ await store.query('Product($condition:@c0,$sort:@s0){ _id, name }', {
168
+ c0: {
169
+ $and: [
170
+ { status: 'onSale' },
171
+ { orders: { $count: { $gt: 3 } } },
172
+ { $not: { orders: { $sum: { $of: 'amount', $gt: 10000 } } } },
173
+ ],
174
+ },
175
+ s0: { name: 1 },
176
+ });
177
+ ```
178
+
179
+ - Shorthand form: `{ "<relation>": { $exists: true | false } }`,
180
+ `{ "$count": { "$of"?: field, "<cmp>": value } }`,
181
+ `{ "$sum" | "$avg" | "$min" | "$max": { "$of": field, "<cmp>": value } }`, optionally with
182
+ a `$filter` block.
183
+ - Main form: `{ filter?, agg, having }`.
184
+ - Comparison operators: `$gt` / `$gte` / `$lt` / `$lte` / `$eq` / `$ne`.
185
+ - Semantics: semi-join — parent row count and document shape are unchanged.
186
+ - `$not` around a single relation predicate, or `$exists: false`, means anti-join.
187
+ - Translates to `EXISTS` / `NOT EXISTS` on SQL and `$lookup` + `$match` on MongoDB.
188
+ - Only **one level** of relation is supported (`orders.items.price` is rejected).
189
+
190
+ ### 6.3 Relation-rolling computed columns
191
+
192
+ Declare once in the schema, request by name in the selection set:
193
+
194
+ ```js
195
+ computes: {
196
+ itemCount: { type: 'int', agg: { $count: 'items' } }, // 0 when the relation is empty
197
+ itemsTotal: { type: 'float', agg: { $sum: 'items.qty' } }, // null when empty
198
+ }
199
+ ```
200
+
201
+ Supported `agg` operators: `$count` / `$sum` / `$avg` / `$min` / `$max`.
202
+
203
+ ---
204
+
205
+ ## 7. Query & write API
206
+
207
+ ```js
208
+ const items = await store.query(gql, params); // Array
209
+ const one = await store.queryOne(gql, params); // object | null
210
+ const page = await store.queryWithCount(gql, params); // { items, total, hasMore, page, pageSize }
211
+ const exists = await store.exists('Post', { _id: pid });
212
+ const n = await store.count('Post', { status: 'active' });
213
+
214
+ const doc = await store.insert('Post', { ... }); // auto _id / createdAt / updatedAt
215
+ const docs = await store.insertMany('Post', [{ ... }, ...]);
216
+ await store.update('Post', { _id: pid }, { status: 'live' }); // plain fields → $set
217
+ await store.update('Post', { _id: pid }, { $inc: { views: 1 } }); // '$'-prefixed keys pass through as operators
218
+ await store.updateMany('Post', { type: t }, { status: 'live' });
219
+ const r = await store.remove('Post', { _id: pid }); // archives to <collection>_deleted first
220
+ await store.mutation('Post', { ... }); // smart upsert + recursive relation children
221
+ await store.upsert('Post', { code: 'A1' }, { ... }); // explicit-condition upsert (no relation handling)
222
+ ```
223
+
224
+ Notes:
225
+
226
+ - `null` / `undefined` values are stripped before persisting; `_id` cannot be changed via `update`.
227
+ - `createdAt` / `updatedAt` (ms) are framework-maintained; do not set them manually.
228
+ - `queryWithCount` accepts `page` / `pageSize` (recommended) or `$skip` / `$limit` params;
229
+ `pageSize` is capped at 5000.
230
+ - `updateMany` / `remove` with an **empty condition** (`{}`, `null`, `{ "$and": [] }`) is
231
+ rejected outright — it never falls through to a full-table write.
232
+
233
+ ---
234
+
235
+ ## 8. Permissions
236
+
237
+ ```js
238
+ store.setContext({ userId: uid, roles: ['editor'] }); // once per request
239
+ store.scopedRoles(['viewer'], () => store.query(gql, params)); // nested-safe role scoping
240
+ await store.runAsInternal(() => store.remove('Post', { _id: pid })); // system / cron jobs
241
+ ```
242
+
243
+ - `super_admin` / `admin` / `internal` pass everything.
244
+ - Other roles are checked against schema-level and field-level `read` / `write` whitelists.
245
+ - `guest` can never write.
246
+ - `creator` is a pseudo-role resolved by `doc.createdBy === ctx.userId`; schemas granting it
247
+ automatically get owner conditions injected on queries and ownership checks on update/remove.
248
+ - No context set → permission checks disabled (backward compatible).
249
+ - Denied access throws `store.PermissionError` with `status = 403`.
250
+
251
+ ### Fail-secure mode (opt-in)
252
+
253
+ ```js
254
+ store.setRequireContext(true);
255
+ // every query/write without a context now throws `ERR_NO_CONTEXT:...`
256
+ await store.runAsInternal(() => store.remove('Post', { _id: pid }));
257
+ ```
258
+
259
+ `runAsInternal` marks the call as `{ internal: true }` (distinct from a missing context, and
260
+ always passes). `setRequireContext(false)` restores the default fail-open behaviour.
261
+
262
+ ---
263
+
264
+ ## 9. Multi-datasource and multi-tenant
265
+
266
+ Every schema is located by the triple `(source, namespace, collection)`, which must be
267
+ globally unique across the registry (duplicate registration throws instead of silently
268
+ mis-routing).
269
+
270
+ - `source` — connection key in `init({...})` (default `"default"`).
271
+ - `namespace` — database/schema inside the connection: Mongo db name, PG schema, MySQL
272
+ database, SQLite attached db. Optional; `null` = connection default.
273
+ - `collection` — table/collection name.
274
+
275
+ ```js
276
+ await init({ mongo_main: db, pg_a: { kind: 'postgres', exec } });
277
+
278
+ await init({ cluster: client });
279
+ store.register({ name: 'User', collection: 'users', datasource: 'cluster', namespace: 'tenant_42', ... });
280
+
281
+ // SQL cross-namespace joins are pushed down natively; only Mongo cross-db relations
282
+ // fall back to in-memory federation.
283
+ ```
284
+
285
+ Multi-tenant route override — one schema definition, N tenants:
286
+
287
+ ```js
288
+ await store.query('User($condition:@c0){...}', params, { namespace: 'tenant_42' });
289
+ await store.insert('Order', data, { source: 'pg_cluster', namespace: 'tenant_7' });
290
+ ```
291
+
292
+ **Security note**: `routeOverride` is a trusted server-side parameter with no origin check.
293
+ Forwarding user-controlled input into it lets a caller re-target another tenant's
294
+ `source` / `namespace` (CWE-639 authorization-bypass surface). Never pass raw request data.
295
+
296
+ ---
297
+
298
+ ## 10. Schema reference
299
+
300
+ ```js
301
+ {
302
+ name: 'Order',
303
+ collection: 'orders',
304
+ idPrefix: 'OD',
305
+ timestamps: true, // auto-maintain createdAt / updatedAt (ms)
306
+ fields: {
307
+ _id: 'string', // shorthand
308
+ title: { type: 'string', default: '' },
309
+ meta: { type: 'object', default: {}, fields: { ... } }, // nested object fields
310
+ },
311
+ relations: {
312
+ items: { model: 'OrderItem', type: 'many', localField: '_id', foreignField: 'orderId' },
313
+ },
314
+ computes: {
315
+ total: { type: 'float', depends: ['amount'], fn: (d) => d.amount * 1.1 },
316
+ itemCount: { type: 'int', agg: { $count: 'items' } },
317
+ },
318
+ indexes: [
319
+ { keys: { status: 1 } },
320
+ { keys: { code: 1 }, options: { unique: true } },
321
+ ],
322
+ read: ['editor', 'viewer'],
323
+ write: ['editor'],
324
+ }
325
+ ```
326
+
327
+ Types: `string | int | long | float | double | boolean | array | object | date | any`.
328
+
329
+ Boundary rules (all fail explicitly, never silently degrade):
330
+
331
+ - Filtering directly on array fields, on a whole object field, or on object dot-paths is
332
+ rejected on every backend. Model cross-entity semantics as `relations` instead.
333
+ - Relation predicates support one level of relation only.
334
+ - An unreadable relation is an error, not a silent `false`.
335
+
336
+ ---
337
+
338
+ ## 11. Advanced API
339
+
340
+ ### `store.buildPipeline(gql, params?)`
341
+
342
+ Compiles GQL to the command plan **without executing it**; returns
343
+ `{ tokens, ast, pipeline, projection }`. Permissions and computes are not applied.
344
+ Useful for debugging query shape, asserting pushdown behaviour, or building tooling such as
345
+ an AI query agent that validates a plan before running it.
346
+
347
+ ```js
348
+ const plan = store.buildPipeline('Post($condition:@c0){ title }', { c0: { status: 'draft' } });
349
+ ```
350
+
351
+ ### `store.syncSchema(opts)`
352
+
353
+ Pulls a SQL backend's physical structure into the registry
354
+ (`introspect → schemaFromRows → mergeSchema(overlay) → register`). It only **reads**
355
+ structure and never writes DDL back.
356
+
357
+ | Option | Type | Meaning |
358
+ | --- | --- | --- |
359
+ | `backend` | `'mysql' \| 'postgres' \| 'sqlite'` | required |
360
+ | `driver` | object | required; prefer a read-only account |
361
+ | `introspectOptions` | object | passed through to introspection (e.g. PG `schema`) |
362
+ | `overlay` | `Array` | local schemaJSON merged on top (permissions / computes / overrides) |
363
+ | `datasource` | string | bind every merged def to this source |
364
+ | `namespace` | string | bind every merged def to this namespace |
365
+ | `registerDefs` | boolean (default `true`) | `false` = return defs without registering |
366
+
367
+ ### `store.setFeedbackSink(fn)`
368
+
369
+ Takes over the unified feedback channel for fallback / degradation / interception events.
370
+ Pass `null` to restore the default stderr printer.
371
+
372
+ ```js
373
+ store.setFeedbackSink((e) => logger.warn({ code: e.code }, e.hint));
374
+ // event shape: { type, code, layer, message, hint, ... }
375
+ // type federation_degraded | sql_pushdown_unsupported | ...
376
+ // code crossSourceSort | pushdownUnsupported | ...
377
+ // layer federation | dialect | ...
378
+ ```
379
+
380
+ Non-pushdownable commands also throw `PushdownUnsupportedError`.
381
+
382
+ ### Low-level modules
383
+
384
+ ```js
385
+ const {
386
+ init, store, Store,
387
+ PermissionError,
388
+ PushdownUnsupportedError,
389
+ datasource, schema, permission, crud, executors, feedback, introspect,
390
+ syncSchema,
391
+ } = require('nodejs-store');
392
+
393
+ const rows = await introspect.run('mysql', pool, {});
394
+ await init({ default: db, pg_a: executors.createConnection('postgres', pgPool) });
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 12. Transaction boundary
400
+
401
+ - **Single SQL source**: `mutation` parent-child step sequences and `remove` (archive +
402
+ delete) run inside one driver transaction on one checked-out connection — any step failure
403
+ rolls back the whole sequence.
404
+ - **Each SQL write command** is itself atomic (multi-statement plans are transaction-wrapped).
405
+ - **Mongo sources**: single-document writes are atomic; multi-step `mutation` and `remove`
406
+ run sequentially and are **not** atomic across steps (Mongo transactions require a replica
407
+ set). Use an SQL source or application-level compensation if you need cross-step atomicity.
408
+ - **Archive idempotency**: `remove` archives with upsert-by-`_id` semantics, so retries after
409
+ partial failure do not collide.
410
+ - **Cross-source steps** cannot be atomic; they run sequentially by design.
411
+
412
+ ---
413
+
414
+ ## 13. FAQ
415
+
416
+ **How do I use one schema for both MongoDB and PostgreSQL in Node.js?**
417
+ Define the schema once as JSON, call `init()` with your datasource(s), and run the same GQL
418
+ against either. MongoDB uses native aggregation; MySQL/PostgreSQL/SQLite get parameterized SQL.
419
+
420
+ **How do I query nested / related data without writing `$lookup` or JOINs?**
421
+ Declare the relation in `relations` (`{ model, type: 'many' | 'one', localField, foreignField }`)
422
+ and reference the relation name in the GQL selection set. It becomes `$lookup` on Mongo and a
423
+ `JOIN` on SQL, returned as nested documents.
424
+
425
+ **Does it support GROUP BY / COUNT / SUM / AVG?**
426
+ Yes — root-level `$group` / `$having` and relation aggregate predicates are part of GQL.
427
+ Operators: `$count` / `$sum` / `$avg` / `$min` / `$max`, pushed down to all four backends.
428
+
429
+ **Can I filter parents by an aggregate of their children ("products with more than 3 orders")?**
430
+ Yes — relation aggregate predicates implement semi/anti-join without fanning out; SQL uses
431
+ `EXISTS` / `NOT EXISTS`.
432
+
433
+ **How do I implement row-level permissions?**
434
+ `store.setContext({ userId, roles })` plus schema-level `read` / `write` whitelists. The
435
+ `creator` pseudo-role adds automatic ownership checks and owner-condition injection. `guest`
436
+ can never write. Enable `setRequireContext(true)` for fail-secure behaviour.
437
+
438
+ **How do I do soft delete?**
439
+ Every registered model automatically gets a `<Model>Deleted` archive collection/table.
440
+ `store.remove()` archives the document first, then deletes it; the archive write is
441
+ upsert-by-`_id`, so re-creating the same `_id` does not collide.
442
+
443
+ **Is it usable for multi-tenant applications?**
444
+ Yes. Bind a schema to `(source, namespace, collection)` and pass a `{ source, namespace }`
445
+ route override per request. Treat `routeOverride` as trusted server-side input only.
446
+
447
+ **Does it run migrations?**
448
+ No. `syncSchema()` only *reads* physical structure via introspection. Schema changes / DDL
449
+ are your migration tool's job.
450
+
451
+ **Can I see the generated query without running it?**
452
+ Yes — `store.buildPipeline(gql, params)` returns the compiled plan with no execution and no
453
+ permission/compute application.
454
+
455
+ **What happens when SQL pushdown isn't possible?**
456
+ The command throws `PushdownUnsupportedError` **and** emits a `sql_pushdown_unsupported`
457
+ feedback event. Cross-source pagination/sort degradations emit `federation_degraded` events.
458
+ Nothing fails silently.
459
+
460
+ **How is it related to py-store and rust-store?**
461
+ `rust-store` is the shared Rust engine (GQL parsing, permissions, computed columns, command
462
+ planning, SQL dialect translation — pure logic, no IO). `nodejs-store` and `py-store` are
463
+ thin hosts in front of it owning driver IO, callbacks and placeholder substitution.
464
+
465
+ ---
466
+
467
+ ## 14. Comparison positioning
468
+
469
+ Short version: use an ORM when you want compile-time types and migrations; use nodejs-store
470
+ when you want one runtime schema and one query dialect spanning MongoDB and SQL, with RBAC
471
+ and computed columns built in.
472
+
473
+ | | nodejs-store | Mongoose | Prisma | TypeORM / Sequelize | Drizzle |
474
+ | --- | --- | --- | --- | --- | --- |
475
+ | Primary shape | JSON schema + GQL data layer | ODM (MongoDB) | Schema DSL + generated client | Decorator/entity ORM | TypeScript SQL builder |
476
+ | Backends | MongoDB, MySQL, SQLite, PostgreSQL | MongoDB | PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, CockroachDB | MySQL, PostgreSQL, SQLite, MSSQL, Oracle (+ MongoDB) | PostgreSQL, MySQL, SQLite, … |
477
+ | One query dialect across Mongo and SQL | yes | no (Mongo only) | no (one client per provider) | partial | no (SQL only) |
478
+ | Nested relation reads in one query | yes | yes (`populate()`) | yes (`include`) | yes | manual joins |
479
+ | Built-in role / field-level RBAC + owner injection | yes | no | no (via extensions) | no | no |
480
+ | Read-time computed columns (sync / async / relation-agg) | yes | getters only | no | no | no |
481
+ | Soft-delete archive table auto-provisioned | yes | no | no | no | no |
482
+ | Migration / DDL engine | no (introspection read-only) | no | yes | yes | yes |
483
+ | Static type generation | no (runtime JSON, cross-language parity) | no | yes | partial | yes |
484
+ | Shared native core across Node and Python | yes (Rust `rust-store`) | no | no | no | no |
485
+
486
+ ---
487
+
488
+ ## 15. Scenario walkthroughs
489
+
490
+ Step-by-step use cases live in
491
+ [`doc/use-cases/`](https://github.com/coenddt/nodejs-store/tree/main/doc/use-cases):
492
+
493
+ 1. **Multi-tenant SaaS** — bind a schema to `(source, namespace, collection)` and re-target each
494
+ request with a trusted `{ source, namespace }` route override.
495
+ 2. **AI data-QA agent** — compile and validate a GQL plan with `buildPipeline()` before
496
+ executing it, and capture degraded / non-pushdownable paths with `setFeedbackSink()`.
497
+ 3. **MongoDB → PostgreSQL migration** — same schema, same GQL, only the `init()` datasource
498
+ changes; introspect the physical structure read-only with `syncSchema()`.
499
+ 4. **Admin CRUD backend** — schema-driven CRUD with computed columns, soft-delete archives,
500
+ role whitelists, the `creator` pseudo-role and `queryWithCount` pagination.
501
+ 5. **Avoiding N+1 reads** — one nested GQL read instead of a parent-then-children loop (a single
502
+ `$lookup` aggregation on MongoDB, a single `LEFT JOIN` statement on SQL), including when the
503
+ planner switches to a two-phase read.
504
+ 6. **Serverless and connection reuse** — hoist `init()` and schema registration, keep the driver
505
+ client warm across invocations, and isolate each request with `setContext` plus fail-secure
506
+ `setRequireContext(true)`.
package/llms.txt ADDED
@@ -0,0 +1,44 @@
1
+ # nodejs-store
2
+
3
+ > Lightweight multi-backend data layer for Node.js: pure JSON schemas plus a MongoDB-style GQL tree query dialect that runs on MongoDB, MySQL, SQLite and PostgreSQL, with built-in role-based access control, computed columns and soft-delete.
4
+
5
+ `nodejs-store` lets a Node.js service define a model once as JSON and query it with a single dialect across MongoDB (native aggregation) and MySQL / PostgreSQL / SQLite (parameterized SQL). Nested relations compile to one native query per backend — no hand-written `$lookup` or JOINs. It is the Node.js host over the shared Rust engine [`rust-store`](https://github.com/coenddt/rust-store) (binding: `rust-store-node`); its Python twin is [`py-store`](https://github.com/coenddt/py-store) (pip `storepy`).
6
+
7
+ - npm package: `nodejs-store`
8
+ - Runtime: Node.js 18+
9
+ - Backends: MongoDB, MySQL, SQLite, PostgreSQL
10
+ - License: MIT
11
+
12
+ ## When to use it
13
+
14
+ - One codebase against several databases (MongoDB in dev, PostgreSQL in prod, or per tenant)
15
+ - Nested / relational reads without writing `$lookup` or JOINs
16
+ - Admin backends and internal CRUD services with schema-driven models
17
+ - Row-level and field-level access control (role whitelists, `creator` ownership, owner-condition injection)
18
+ - Multi-tenant SaaS (schema bound to `source` / `namespace`, runtime route override)
19
+ - Migrating between MongoDB and SQL while keeping one query syntax
20
+ - AI / natural-language data-QA layers (plan-only `buildPipeline()`, deterministic command JSON, structured feedback events)
21
+
22
+ ## Docs
23
+
24
+ - [README](https://github.com/coenddt/nodejs-store#readme): full overview, quick start, GQL syntax, API, schema reference, FAQ
25
+ - [Chinese README](https://github.com/coenddt/nodejs-store/blob/main/README.zh-CN.md): 中文文档
26
+ - [Use cases](https://github.com/coenddt/nodejs-store/tree/main/doc/use-cases): scenario walkthroughs (multi-tenant SaaS, AI data-QA, cross-database migration, admin CRUD, avoiding N+1 reads, serverless connection reuse)
27
+ - [Scenario test suite](https://github.com/coenddt/nodejs-store/tree/main/example/course-platform): real-data, four-backend scenario cases (fields, relations, computes, permissions, aggregation, boundaries)
28
+ - [CHANGELOG](https://github.com/coenddt/nodejs-store/blob/main/CHANGELOG.md)
29
+
30
+ ## Concepts
31
+
32
+ - [GQL tree syntax](https://github.com/coenddt/nodejs-store#gql-syntax): `Model($condition:@c0,$sort:@s1,$limit:@l){ field, Relation{ f } }`
33
+ - [Aggregation](https://github.com/coenddt/nodejs-store#aggregation): root-level `$group` / `$having`, relation aggregate predicates (semi/anti-join), relation-rolling computed columns
34
+ - [Query & write API](https://github.com/coenddt/nodejs-store#query--write-api): `query`, `queryOne`, `queryWithCount`, `insert`, `update`, `remove`, `mutation`, `upsert`
35
+ - [Permissions](https://github.com/coenddt/nodejs-store#permission-context): `setContext`, `scopedRoles`, `runAsInternal`, fail-secure `setRequireContext(true)`
36
+ - [Multi-datasource & multi-tenant](https://github.com/coenddt/nodejs-store#multi-datasource-connections): `(source, namespace, collection)` and route override
37
+ - [Schema reference](https://github.com/coenddt/nodejs-store#schema-reference): fields, relations, computes, indexes, read/write whitelists
38
+ - [Advanced API](https://github.com/coenddt/nodejs-store#advanced-api): `buildPipeline`, `syncSchema`, `setFeedbackSink`, low-level modules
39
+ - [Transactions](https://github.com/coenddt/nodejs-store#transaction-boundary): per-source atomicity guarantees
40
+
41
+ ## Optional
42
+
43
+ - [py-store (Python twin)](https://github.com/coenddt/py-store): same schemas, same GQL, snake_case API
44
+ - [rust-store (shared Rust core)](https://github.com/coenddt/rust-store): GQL parsing, permissions, computed columns, command planning, SQL dialect translation
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "nodejs-store",
3
- "version": "2.0.0",
4
- "description": "Lightweight multi-backend data layer (MongoDB / MySQL / SQLite / PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single query, computed columns, soft-delete and role-based access control",
3
+ "version": "2.0.2",
4
+ "description": "Multi-backend data layer for Node.js (MongoDB, MySQL, SQLite, PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control",
5
5
  "main": "src/index.js",
6
6
  "files": [
7
7
  "src",
8
8
  "README.md",
9
+ "README.zh-CN.md",
10
+ "llms.txt",
11
+ "llms-full.txt",
9
12
  "LICENSE"
10
13
  ],
11
14
  "scripts": {
12
15
  "test": "node scripts/test.js",
13
- "test:coverage": "c8 --all --include src/** -x **/rust-store/** --reporter=text --reporter=html --check-coverage --statements 90 --lines 90 --functions 85 --branches 75 node scripts/test.js",
16
+ "test:coverage": "c8 --all --include \"src/**\" -x \"**/rust-store/**\" --reporter=text --reporter=html --check-coverage --statements 90 --lines 90 --functions 85 --branches 75 node scripts/test.js",
14
17
  "lint": "eslint ."
15
18
  },
16
19
  "keywords": [
@@ -18,12 +21,31 @@
18
21
  "mysql",
19
22
  "sqlite",
20
23
  "postgresql",
24
+ "postgres",
21
25
  "data-layer",
22
26
  "query-builder",
23
27
  "odm",
28
+ "orm-alternative",
29
+ "mongoose-alternative",
30
+ "prisma-alternative",
24
31
  "gql",
32
+ "json-schema",
33
+ "multi-database",
34
+ "cross-database",
35
+ "multi-tenant",
25
36
  "acl",
26
- "aggregation"
37
+ "rbac",
38
+ "access-control",
39
+ "row-level-security",
40
+ "aggregation",
41
+ "group-by",
42
+ "computed-columns",
43
+ "soft-delete",
44
+ "ai-agent",
45
+ "llm-tool",
46
+ "query-validation",
47
+ "natural-language-query",
48
+ "node"
27
49
  ],
28
50
  "author": "leo <coen_ddt@qq.com>",
29
51
  "license": "MIT",