json-rest-api 2.0.0 → 2.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/docs/GUIDE/21-schema-and-migrations.md +112 -13
- package/docs/GUIDE/30-backend-capabilities.md +8 -5
- package/docs/GUIDE/33-migrating-to-v2.md +18 -7
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/lib/knex-transaction.js +10 -4
- package/package.json +1 -1
- package/plugins/core/lib/anyapi/anyapi-registry.js +528 -409
- package/plugins/core/lib/anyapi/schema-utils.js +101 -46
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +235 -51
- package/types/anyapi.d.ts +30 -0
- package/types/plugin-options.d.ts +12 -0
|
@@ -307,26 +307,125 @@ separate DDL behavior.
|
|
|
307
307
|
|
|
308
308
|
Existing rows are not backfilled: unused slots stay null, and defaults apply to
|
|
309
309
|
subsequent writes. Function defaults do not run during allocation. Backfill old
|
|
310
|
-
rows explicitly when needed.
|
|
311
|
-
|
|
310
|
+
rows explicitly when needed.
|
|
311
|
+
|
|
312
|
+
Additions are stored with their search schema, so they survive a restart
|
|
313
|
+
without being declared again. Persisted JSON cannot hold callbacks: getters,
|
|
314
|
+
setters, function defaults, serializers and `compute` functions exist only in the
|
|
315
|
+
process that added them. A server that loads such a field without declaring it
|
|
316
|
+
in code hides the field and logs a warning instead of serving it without its
|
|
317
|
+
callbacks. Declare fields with callbacks in app source on every server; a
|
|
318
|
+
declared field always takes precedence over a stored addition of the same name.
|
|
312
319
|
|
|
313
320
|
Explicit maps now take effect during registration. Repeating `createKnexTable()`
|
|
314
321
|
ensures canonical tables and refreshes schema information without replacing
|
|
315
322
|
field metadata or removing additions.
|
|
316
323
|
|
|
317
|
-
Canonical registration allocates slots from the enriched compiled definitions
|
|
318
|
-
|
|
319
|
-
once per registration
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
order
|
|
325
|
-
the
|
|
324
|
+
Canonical registration allocates slots from the enriched compiled definitions.
|
|
325
|
+
Stored additions that the declaration does not repeat are compiled from their
|
|
326
|
+
persisted JSON. Schema/search hooks run once per registration, `addKnexFields`
|
|
327
|
+
compilation or reload that changes the field set. Repeated table creation does
|
|
328
|
+
not rerun them. Search hook changes therefore survive descriptor refresh.
|
|
329
|
+
|
|
330
|
+
Registration never moves a stored field: existing fields keep their slots
|
|
331
|
+
whatever the declaration order, and new fields take the first free slot. A field
|
|
332
|
+
the declaration omits keeps its mapping while the resource has records, because
|
|
333
|
+
servers still running older code may use it; this declaration stops publishing
|
|
334
|
+
it. Unpopulated omitted fields are released. Changing a populated field's storage
|
|
335
|
+
type is still rejected and needs an explicit data migration. See the
|
|
336
|
+
[schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
|
|
326
337
|
including the consequences for hooks that were previously overwritten.
|
|
327
338
|
|
|
328
|
-
|
|
329
|
-
|
|
339
|
+
Registering an unchanged declaration writes nothing. Concurrent schema changes
|
|
340
|
+
to one resource queue on its metadata row, and a unique index stops two fields
|
|
341
|
+
from sharing a slot. Each committed change increments the resource's
|
|
342
|
+
`schema_version`. On MySQL, the registry runs its own transactions at READ
|
|
343
|
+
COMMITTED, because REPEATABLE READ makes concurrent creation of different
|
|
344
|
+
resources or tenants deadlock. A transaction you pass in keeps its own isolation
|
|
345
|
+
level.
|
|
346
|
+
|
|
347
|
+
### AnyAPI across several servers
|
|
348
|
+
|
|
349
|
+
Each server compiles resources into memory. When one server adds fields, the
|
|
350
|
+
others keep their published schema until they reload it:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
await api.use(RestApiAnyapiKnexPlugin, {
|
|
354
|
+
knex,
|
|
355
|
+
tenantId,
|
|
356
|
+
// Runs after each committed change; publish it to the other servers.
|
|
357
|
+
onSchemaChanged: change => notifyServers(change),
|
|
358
|
+
// Safety net for missed notifications.
|
|
359
|
+
schemaCheckInterval: 5000
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// On a server that receives a notification:
|
|
363
|
+
await api.anyapi.handleSchemaChange(change)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`handleSchemaChange({ tenant, resource, version })` reloads the resource only when
|
|
367
|
+
the version is newer than the published one, and returns whether it did.
|
|
368
|
+
`api.resources.items.reloadSchema()` reloads unconditionally and resolves to the
|
|
369
|
+
published version. Reloads are read-only: they combine the stored definition
|
|
370
|
+
with this server's declaration and never re-register it.
|
|
371
|
+
|
|
372
|
+
With `schemaCheckInterval`, request methods compare the stored version at most
|
|
373
|
+
once per interval; `0` checks on every request. A request rejected by validation
|
|
374
|
+
also checks the version once and, if the resource changed, reloads and retries.
|
|
375
|
+
A write whose transaction may have committed is never retried. Calls given a
|
|
376
|
+
caller transaction skip both checks, because that transaction may hold
|
|
377
|
+
uncommitted metadata. A rejected `onSchemaChanged` callback is logged; the
|
|
378
|
+
committed change stands.
|
|
379
|
+
|
|
380
|
+
A server can serve resources it never declared. `await api.anyapi.loadResource('items')`
|
|
381
|
+
loads a stored resource, with the resources it relates to, and resolves to it, or to
|
|
382
|
+
`null` when none is stored. `api.anyapi.loadResources()` loads every stored resource
|
|
383
|
+
of the tenant. A stored resource whose non-field options needed callbacks, such as
|
|
384
|
+
`normalizeId`, must be declared in code. To load resources on demand over HTTP,
|
|
385
|
+
call `loadResource` from middleware placed before the API router.
|
|
386
|
+
|
|
387
|
+
### AnyAPI tenant instances
|
|
388
|
+
|
|
389
|
+
One API instance per tenant stays cheap when the instances share their start-up work:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
// In a migration:
|
|
393
|
+
await ensureAnyApiSchema(knex, { slots: { string: 60, number: 30, boolean: 20, date: 20, json: 10, belongsTo: 15 } })
|
|
394
|
+
|
|
395
|
+
// In the application:
|
|
396
|
+
const registry = new AnyapiRegistry({ knex, cacheSize: 5000 })
|
|
397
|
+
const tenantApi = async tenantId => {
|
|
398
|
+
const api = new JsonRestApi()
|
|
399
|
+
await api.use(RestApiPlugin)
|
|
400
|
+
await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId, registry, ensureSchema: false, ownsKnex: false })
|
|
401
|
+
await api.anyapi.loadResources()
|
|
402
|
+
return api
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
A shared registry reads database capabilities and the slot layout once, and holds
|
|
407
|
+
one descriptor cache, bounded by `cacheSize` (default 100). `ensureSchema: false`
|
|
408
|
+
skips table checks and the logical ID backfill at start-up and in `createKnexTable()`;
|
|
409
|
+
migrations must then run `ensureAnyApiSchema`. `ownsKnex: false` keeps `api.release()`
|
|
410
|
+
from destroying the shared Knex instance. A dropped instance needs no other cleanup.
|
|
411
|
+
|
|
412
|
+
### AnyAPI slot limits
|
|
413
|
+
|
|
414
|
+
Slot limits belong to the database: every server reads the slot columns that
|
|
415
|
+
`any_records` actually has. The defaults are 10 string, 10 number, 5 boolean, 5
|
|
416
|
+
date, 5 JSON and 5 belongsTo slots. `ensureAnyApiSchema(knex, { slots })`, or the
|
|
417
|
+
plugin's `slots` option when it ensures the schema, adds missing columns. Lower
|
|
418
|
+
counts never drop columns. A server whose pool is exhausted rereads the layout
|
|
419
|
+
once, so a migration that adds slots takes effect without a restart.
|
|
420
|
+
|
|
421
|
+
On PostgreSQL, adding a nullable column without a default does not rewrite the
|
|
422
|
+
table. MySQL can add columns instantly from 8.0.29; earlier versions may copy the
|
|
423
|
+
table. MySQL limits a row to 65,535 bytes and each belongsTo slot adds two
|
|
424
|
+
string columns, so check the row size before raising that pool substantially.
|
|
425
|
+
|
|
426
|
+
Deep `AnyapiRegistry.allocateField` and `allocateFields` calls persist the field
|
|
427
|
+
definitions and their allocation together. `allocateFields` adds a batch under one
|
|
428
|
+
version; pass each field's uncompiled `declaration` to store it for other servers. Borrowed transactions own their reads/writes and bypass the
|
|
330
429
|
global descriptor cache; the registry does not commit, roll back or publish their
|
|
331
430
|
uncommitted descriptors globally. After commit, call
|
|
332
431
|
`invalidateDescriptor(tenant, resource)` before an ordinary cached read. Deep
|
|
@@ -62,11 +62,14 @@ changes can be omitted with warnings, and destructive `down` migrations are not
|
|
|
62
62
|
generated automatically. SQLite check/enum changes can require a reviewed table
|
|
63
63
|
rebuild. Native SET fields require an explicit MySQL target.
|
|
64
64
|
|
|
65
|
-
AnyAPI field additions persist slot metadata
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
AnyAPI field additions persist slot metadata and their definitions;
|
|
66
|
+
`alterKnexFields` is unsupported. Registration keeps stored fields in their slots
|
|
67
|
+
and keeps the mapping of populated fields a declaration omits; changing a populated
|
|
68
|
+
field's storage type needs an explicit data migration. Servers sharing a database
|
|
69
|
+
see each other's additions after a reload, notification or schema check, and slot
|
|
70
|
+
limits follow the columns the database has. Old canonical temporal storage needs
|
|
71
|
+
the documented data migration, not just a package update. See
|
|
72
|
+
[table helpers and migration limits](21-schema-and-migrations.md#anyapi-across-several-servers).
|
|
70
73
|
|
|
71
74
|
## Transaction ownership and failures
|
|
72
75
|
|
|
@@ -1466,6 +1466,16 @@ Missing `fields`, duplicate fields and invalid maps now throw instead of silentl
|
|
|
1466
1466
|
succeeding or leaving unusable metadata. See
|
|
1467
1467
|
[AnyAPI field additions](21-schema-and-migrations.md#anyapi-field-additions).
|
|
1468
1468
|
|
|
1469
|
+
`ensureAnyApiSchema` now adds `schema_version` and `definition_json` to
|
|
1470
|
+
`any_resource_configs` and a unique slot index to `any_field_configs`. It refuses
|
|
1471
|
+
to add the index while two fields of one resource share a slot, naming the slot;
|
|
1472
|
+
reassign one of them first. Deployments using `ensureSchema: false` must run
|
|
1473
|
+
`ensureAnyApiSchema` in a migration. Registration no longer rejects a declaration
|
|
1474
|
+
that omits a populated field or reorders fields: stored slots stay where they
|
|
1475
|
+
are, and omitted populated fields keep their data without being published.
|
|
1476
|
+
Fields added before this release that the declaration does not repeat are kept
|
|
1477
|
+
and published from their stored definitions.
|
|
1478
|
+
|
|
1469
1479
|
Explicit maps take effect during registration, before `createKnexTable()`.
|
|
1470
1480
|
Repeating that method no longer re-registers the initial schema and discards
|
|
1471
1481
|
additions. Keep the expanded declaration and complete map in app source for
|
|
@@ -2805,13 +2815,14 @@ compilation. Canonical metadata no longer invents an attribute declaration for
|
|
|
2805
2815
|
an implicit resource ID; metadata consumers should use `schemaInfo.idProperty`
|
|
2806
2816
|
to identify the resource ID configuration.
|
|
2807
2817
|
|
|
2808
|
-
Enrichment runs once at resource registration,
|
|
2809
|
-
`addKnexFields` compilation
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
schema; use the resource helper
|
|
2813
|
-
|
|
2814
|
-
functions
|
|
2818
|
+
Enrichment runs once at resource registration, once for each canonical
|
|
2819
|
+
`addKnexFields` compilation, and once for a reload that changes the field set.
|
|
2820
|
+
Repeated canonical `createKnexTable()` refreshes storage metadata without
|
|
2821
|
+
rerunning enrichment or restoring removed search fields. Deep registry allocation
|
|
2822
|
+
does not update a running resource's compiled schema; use the resource helper or
|
|
2823
|
+
`reloadSchema()`. Additions survive restarts without being declared again, but
|
|
2824
|
+
persisted JSON cannot restore functions: declare fields with callbacks in source,
|
|
2825
|
+
or servers hide them. Ordinary `addKnexFields` retains its DDL-only behavior.
|
|
2815
2826
|
|
|
2816
2827
|
Fastify now resolves the current cached request contract when validating a
|
|
2817
2828
|
payload. A belongs-to alias added through canonical `addKnexFields` can be used
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type * from './types/relationship-methods.js'
|
|
|
6
6
|
export type * from './types/bulk-methods.js'
|
|
7
7
|
export type { ManagedTransaction, ManagedTransactionParam, TransactionMethods } from './types/transactions.js'
|
|
8
8
|
export type * from './types/plugin-options.js'
|
|
9
|
+
export * from './types/anyapi.js'
|
|
9
10
|
export type * from './types/file-detectors.js'
|
|
10
11
|
export type * from './types/fastify-options.js'
|
|
11
12
|
export type * from './types/row-policy.js'
|
package/index.js
CHANGED
|
@@ -13,6 +13,8 @@ export { SocketIOPlugin } from './plugins/core/socketio-plugin.js'
|
|
|
13
13
|
// Database plugins
|
|
14
14
|
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js'
|
|
15
15
|
export { RestApiAnyapiKnexPlugin } from './plugins/core/rest-api-anyapi-knex-plugin.js'
|
|
16
|
+
export { AnyapiRegistry } from './plugins/core/lib/anyapi/anyapi-registry.js'
|
|
17
|
+
export { ensureAnyApiSchema } from './plugins/core/lib/anyapi/schema-utils.js'
|
|
16
18
|
|
|
17
19
|
// Connector plugins
|
|
18
20
|
export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js'
|
package/lib/knex-transaction.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** @import { OwnedTransaction, TransactionOutcome } from './transaction-types.js' */
|
|
3
3
|
/** @import { StorageDatabase } from '../plugins/core/lib/storage/storage-types.js' */
|
|
4
|
-
/** @typedef {{ cleanupErrors?: Array<{ phase: string, error: unknown }>, [serializableTransaction]?: boolean }} CleanupContext */
|
|
4
|
+
/** @typedef {{ cleanupErrors?: Array<{ phase: string, error: unknown }>, [serializableTransaction]?: boolean, [readCommittedTransaction]?: boolean }} CleanupContext */
|
|
5
5
|
/** @type {WeakMap<OwnedTransaction, (error?: unknown) => Promise<void>>} */
|
|
6
6
|
const transactionConnections = new WeakMap()
|
|
7
7
|
|
|
8
8
|
// Internal connector request; HTTP input cannot select transaction isolation.
|
|
9
9
|
export const serializableTransaction = Symbol('serializableTransaction')
|
|
10
|
+
// MySQL's default REPEATABLE READ gap-locks locking reads of missing rows, so
|
|
11
|
+
// concurrent creators of different rows deadlock on insert. PostgreSQL and
|
|
12
|
+
// SQLite already behave as requested.
|
|
13
|
+
export const readCommittedTransaction = Symbol('readCommittedTransaction')
|
|
10
14
|
|
|
11
15
|
// Own the lease without changing the driver's SQL-completion promise.
|
|
12
16
|
/**
|
|
@@ -30,9 +34,11 @@ export async function createKnexTransaction (knex, context = {}) {
|
|
|
30
34
|
return releasePromise
|
|
31
35
|
}
|
|
32
36
|
try {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
37
|
+
const client = knex.client.config.client
|
|
38
|
+
const isolationLevel = context[serializableTransaction] === true
|
|
39
|
+
? !['sqlite3', 'better-sqlite3'].includes(client) && 'serializable'
|
|
40
|
+
: context[readCommittedTransaction] === true && ['mysql', 'mysql2'].includes(client) && 'read committed'
|
|
41
|
+
transaction = await knex.transaction({ connection, ...(isolationLevel ? { isolationLevel } : {}) })
|
|
36
42
|
transactionConnections.set(transaction, release)
|
|
37
43
|
transaction.executionPromise.then(
|
|
38
44
|
() => release(),
|