@oimlsmart/platform-server 0.2.7 → 0.2.8
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/package.json +1 -1
- package/src/store/d1.ts +35 -6
- package/src/store/sqlite/entities.ts +35 -7
- package/src/store/sqlite.ts +5 -1
- package/src/store.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "The OIML SMART platform server kernel: the store seam (ServerStore + the D1 and SQLite implementations), the canonical D1 migration set both deployments apply, the instance profile, the mailer, the RBAC map, the OIDC/OAuth client cones, and the shared role/permission vocabulary. Consumed by the smart monorepo (browser/) and the identity service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/store/d1.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { D1Database, D1PreparedStatement } from '@cloudflare/workers-types'
|
|
|
32
32
|
import {
|
|
33
33
|
DEMO_PASSWORD,
|
|
34
34
|
EVENTS_BULK_KEY_CHUNK,
|
|
35
|
+
PUT_ENTITIES_CHUNK,
|
|
35
36
|
StoreUnavailable,
|
|
36
37
|
type AccountEmail,
|
|
37
38
|
type AddAccountEmailResult,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
type EnrollmentToken,
|
|
45
46
|
type EntityChange,
|
|
46
47
|
type EntityRow,
|
|
48
|
+
type EntityWriteInput,
|
|
47
49
|
type EventEntityKey,
|
|
48
50
|
type EventKeyFilter,
|
|
49
51
|
type FederationPeer,
|
|
@@ -413,6 +415,14 @@ function boundedD1Writes(binding: D1Database, budgetMs: number): D1Database {
|
|
|
413
415
|
}
|
|
414
416
|
}
|
|
415
417
|
|
|
418
|
+
/** The entity write's two statements, ONE textual source for putEntity
|
|
419
|
+
* and putEntities alike (the multi-row write must land each row
|
|
420
|
+
* byte-identically to the single-row verb — the upsert, then its
|
|
421
|
+
* journal entry). */
|
|
422
|
+
const ENTITY_UPSERT_SQL = `INSERT INTO entities (store, id, org_id, data, updated_at) VALUES (?, ?, ?, ?, datetime('now'))
|
|
423
|
+
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`
|
|
424
|
+
const ENTITY_CHANGE_SQL = 'INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)'
|
|
425
|
+
|
|
416
426
|
export class D1ServerStore implements ServerStore {
|
|
417
427
|
/** The RAW binding — the ensure memos (and d1StoreFor's map) key on
|
|
418
428
|
* it: the facade below is per-instance and would never hit. */
|
|
@@ -3450,15 +3460,34 @@ export class D1ServerStore implements ServerStore {
|
|
|
3450
3460
|
// all-or-nothing, the same atomicity the SQLite path gets from its
|
|
3451
3461
|
// transaction.
|
|
3452
3462
|
await this.db.batch([
|
|
3453
|
-
this.stmt(
|
|
3454
|
-
|
|
3455
|
-
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`,
|
|
3456
|
-
store, id, orgId, data,
|
|
3457
|
-
),
|
|
3458
|
-
this.stmt('INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)', store, 'persist', id),
|
|
3463
|
+
this.stmt(ENTITY_UPSERT_SQL, store, id, orgId, data),
|
|
3464
|
+
this.stmt(ENTITY_CHANGE_SQL, store, 'persist', id),
|
|
3459
3465
|
])
|
|
3460
3466
|
}
|
|
3461
3467
|
|
|
3468
|
+
async putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void> {
|
|
3469
|
+
// The seam's contract, mechanically: each row lands exactly as
|
|
3470
|
+
// putEntity would land it (the upsert, then its journal entry), the
|
|
3471
|
+
// statements ride each batch in INPUT order, and the chunks issue
|
|
3472
|
+
// SERIALLY — so the journal's seq order IS the input's row order
|
|
3473
|
+
// (the audit-chain property the CSV commit relies on). The chunk is
|
|
3474
|
+
// the atomic unit: a D1 batch lands all-or-nothing; a failed chunk
|
|
3475
|
+
// throws with its rows unlanded, earlier chunks standing, later
|
|
3476
|
+
// chunks never issued.
|
|
3477
|
+
if (rows.length === 0) return
|
|
3478
|
+
for (let i = 0; i < rows.length; i += PUT_ENTITIES_CHUNK) {
|
|
3479
|
+
const chunk = rows.slice(i, i + PUT_ENTITIES_CHUNK)
|
|
3480
|
+
const statements: D1PreparedStatement[] = []
|
|
3481
|
+
for (const row of chunk) {
|
|
3482
|
+
statements.push(
|
|
3483
|
+
this.stmt(ENTITY_UPSERT_SQL, store, row.id, row.orgId, row.data),
|
|
3484
|
+
this.stmt(ENTITY_CHANGE_SQL, store, 'persist', row.id),
|
|
3485
|
+
)
|
|
3486
|
+
}
|
|
3487
|
+
await this.db.batch(statements)
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3462
3491
|
async deleteEntity(store: string, id: string): Promise<boolean> {
|
|
3463
3492
|
const res = await this.stmt('DELETE FROM entities WHERE store = ? AND id = ?', store, id).run()
|
|
3464
3493
|
const gone = (res.meta.changes ?? 0) > 0
|
|
@@ -13,12 +13,20 @@
|
|
|
13
13
|
// ═══════════════════════════════════════════════════════════════════
|
|
14
14
|
|
|
15
15
|
import { getDb } from './store'
|
|
16
|
-
import { orgIdOf } from '../../store'
|
|
16
|
+
import { PUT_ENTITIES_CHUNK, orgIdOf } from '../../store'
|
|
17
17
|
|
|
18
18
|
export type { EntityRow, EntityChange } from '../../store'
|
|
19
19
|
export { ORG_FIELDS, CATALOG_STORES, orgIdOf } from '../../store'
|
|
20
20
|
|
|
21
|
-
import type { EntityRow, EntityChange } from '../../store'
|
|
21
|
+
import type { EntityRow, EntityChange, EntityWriteInput } from '../../store'
|
|
22
|
+
|
|
23
|
+
/** The entity write's two statements, ONE textual source for putEntity
|
|
24
|
+
* and putEntities alike (the multi-row write must land each row
|
|
25
|
+
* byte-identically to the single-row verb — the upsert, then its
|
|
26
|
+
* journal entry). */
|
|
27
|
+
const ENTITY_UPSERT_SQL = `INSERT INTO entities (store, id, org_id, data, updated_at) VALUES (?, ?, ?, ?, datetime('now'))
|
|
28
|
+
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`
|
|
29
|
+
const ENTITY_CHANGE_SQL = 'INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)'
|
|
22
30
|
|
|
23
31
|
export function listEntities(store: string): EntityRow[] {
|
|
24
32
|
// The ORDER BY is the seam's contract (the 0.2.3 pin, the D1 half's
|
|
@@ -40,15 +48,35 @@ export function getEntity(store: string, id: string): EntityRow | undefined {
|
|
|
40
48
|
export function putEntity(store: string, id: string, orgId: string | null, data: string): void {
|
|
41
49
|
const db = getDb()
|
|
42
50
|
const write = db.transaction(() => {
|
|
43
|
-
db.prepare(
|
|
44
|
-
|
|
45
|
-
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`,
|
|
46
|
-
).run(store, id, orgId, data)
|
|
47
|
-
db.prepare('INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)').run(store, 'persist', id)
|
|
51
|
+
db.prepare(ENTITY_UPSERT_SQL).run(store, id, orgId, data)
|
|
52
|
+
db.prepare(ENTITY_CHANGE_SQL).run(store, 'persist', id)
|
|
48
53
|
})
|
|
49
54
|
write()
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/** The multi-row write (the seam's putEntities, the 2026-09-07 audit's
|
|
58
|
+
* J1): each row lands exactly as putEntity would land it, in INPUT
|
|
59
|
+
* order, one transaction per PUT_ENTITIES_CHUNK rows — the chunk is
|
|
60
|
+
* the atomic unit, matching the D1 batch's all-or-nothing; a failed
|
|
61
|
+
* chunk throws with its rows unlanded, earlier chunks standing, later
|
|
62
|
+
* chunks never issued. The chunks run serially, so the journal's seq
|
|
63
|
+
* order IS the input's row order. */
|
|
64
|
+
export function putEntities(store: string, rows: readonly EntityWriteInput[]): void {
|
|
65
|
+
if (rows.length === 0) return
|
|
66
|
+
const db = getDb()
|
|
67
|
+
const upsert = db.prepare(ENTITY_UPSERT_SQL)
|
|
68
|
+
const journal = db.prepare(ENTITY_CHANGE_SQL)
|
|
69
|
+
for (let i = 0; i < rows.length; i += PUT_ENTITIES_CHUNK) {
|
|
70
|
+
const chunk = rows.slice(i, i + PUT_ENTITIES_CHUNK)
|
|
71
|
+
db.transaction(() => {
|
|
72
|
+
for (const row of chunk) {
|
|
73
|
+
upsert.run(store, row.id, row.orgId, row.data)
|
|
74
|
+
journal.run(store, 'persist', row.id)
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
export function deleteEntity(store: string, id: string): boolean {
|
|
53
81
|
const db = getDb()
|
|
54
82
|
let gone = false
|
package/src/store/sqlite.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
latestChangeSeq,
|
|
87
87
|
latestChangeSeqFor,
|
|
88
88
|
listEntities,
|
|
89
|
+
putEntities,
|
|
89
90
|
putEntity,
|
|
90
91
|
} from './sqlite/entities'
|
|
91
92
|
import {
|
|
@@ -186,7 +187,7 @@ import {
|
|
|
186
187
|
updateOpAccount,
|
|
187
188
|
updateUserName,
|
|
188
189
|
} from './sqlite/op-accounts-store'
|
|
189
|
-
import { installStore, type AccountEmail, type AddAccountEmailResult, type AuthUserPayload, type CertificateHolderClaim, type CertificateHolderOrg, type CompleteEmailChangeResult, type CompleteEnrollmentResult, type ConsumeOidcRefreshTokenResult, type EmailChangeToken, type EnrollmentToken, type EntityChange, type EntityRow, type EventEntityKey, type EventKeyFilter, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type NotifyDelivery, type NotifyDeliveryStatus, type NotifyEntityMute, type NotifyInboxState, type NotifyPreferences, type NotifyRule, type OAuthInitialAssignment, type OidcAccessToken, type OidcAuthorization, type OidcClient, type OidcClientLaunch, type OidcCode, type OidcConsentGrant, type OidcKeyRow, type OidcRefreshToken, type OpAccountErasure, type OpClientRoleAssignment, type OpLiveSession, type OrgJoinRequest, type OrgMembership, type OrgMembershipState, type OrgRegistryContact, type OrgRegistryOrg, type OrgRegistryState, type PersonalAccessToken, type PlatformEvent, type ServerStore, type SessionView, type SsoSignInState, type UserAdminRow, type AdvanceCounterResult, type MfaPending, type RecoveryCodeState, type TotpSecret, type WebauthnChallenge, type WebauthnCredential } from '../store'
|
|
190
|
+
import { installStore, type AccountEmail, type AddAccountEmailResult, type AuthUserPayload, type CertificateHolderClaim, type CertificateHolderOrg, type CompleteEmailChangeResult, type CompleteEnrollmentResult, type ConsumeOidcRefreshTokenResult, type EmailChangeToken, type EnrollmentToken, type EntityChange, type EntityRow, type EntityWriteInput, type EventEntityKey, type EventKeyFilter, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type NotifyDelivery, type NotifyDeliveryStatus, type NotifyEntityMute, type NotifyInboxState, type NotifyPreferences, type NotifyRule, type OAuthInitialAssignment, type OidcAccessToken, type OidcAuthorization, type OidcClient, type OidcClientLaunch, type OidcCode, type OidcConsentGrant, type OidcKeyRow, type OidcRefreshToken, type OpAccountErasure, type OpClientRoleAssignment, type OpLiveSession, type OrgJoinRequest, type OrgMembership, type OrgMembershipState, type OrgRegistryContact, type OrgRegistryOrg, type OrgRegistryState, type PersonalAccessToken, type PlatformEvent, type ServerStore, type SessionView, type SsoSignInState, type UserAdminRow, type AdvanceCounterResult, type MfaPending, type RecoveryCodeState, type TotpSecret, type WebauthnChallenge, type WebauthnCredential } from '../store'
|
|
190
191
|
import {
|
|
191
192
|
advanceWebauthnCounter,
|
|
192
193
|
consumeMfaPending,
|
|
@@ -1014,6 +1015,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
1014
1015
|
async putEntity(store: string, id: string, orgId: string | null, data: string): Promise<void> {
|
|
1015
1016
|
putEntity(store, id, orgId, data)
|
|
1016
1017
|
},
|
|
1018
|
+
async putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void> {
|
|
1019
|
+
putEntities(store, rows)
|
|
1020
|
+
},
|
|
1017
1021
|
async deleteEntity(store: string, id: string): Promise<boolean> {
|
|
1018
1022
|
return deleteEntity(store, id)
|
|
1019
1023
|
},
|
package/src/store.ts
CHANGED
|
@@ -175,6 +175,26 @@ export interface EntityChange {
|
|
|
175
175
|
at: string
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** One row of the multi-row write (putEntities): putEntity's (id,
|
|
179
|
+
* orgId, data) legs — the store is the call's, once for the whole
|
|
180
|
+
* batch. */
|
|
181
|
+
export interface EntityWriteInput {
|
|
182
|
+
id: string
|
|
183
|
+
orgId: string | null
|
|
184
|
+
data: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The multi-row write's batch chunk, in ROWS (putEntities): each row
|
|
188
|
+
* contributes two statements (the upsert + its journal entry), so a
|
|
189
|
+
* chunk is one db.batch of 2 × PUT_ENTITIES_CHUNK statements — the
|
|
190
|
+
* same conservative order as EVENTS_BULK_KEY_CHUNK. The chunk bounds
|
|
191
|
+
* the ATOMIC unit (a D1 batch is all-or-nothing; the SQLite half's
|
|
192
|
+
* per-chunk transaction matches it) and keeps one batch's latency
|
|
193
|
+
* well inside the write budget at demo-hub latency. Chunks issue
|
|
194
|
+
* SERIALLY, in input order — the journal's seq order IS the input
|
|
195
|
+
* order, and parallel chunks would forfeit it. */
|
|
196
|
+
export const PUT_ENTITIES_CHUNK = 50
|
|
197
|
+
|
|
178
198
|
// ── the platform event store (TODO.notify/01) ────────────────────────
|
|
179
199
|
|
|
180
200
|
/** A notifiable platform event (TODO.notify/00's event model): one row
|
|
@@ -2349,6 +2369,33 @@ export interface ServerStore {
|
|
|
2349
2369
|
listEntities(store: string): Promise<EntityRow[]>
|
|
2350
2370
|
getEntity(store: string, id: string): Promise<EntityRow | undefined>
|
|
2351
2371
|
putEntity(store: string, id: string, orgId: string | null, data: string): Promise<void>
|
|
2372
|
+
/** The MULTI-ROW write (the 2026-09-07 performance audit's J1 — the
|
|
2373
|
+
* CSV registration commit awaited putEntity per imported row, ~10⁴
|
|
2374
|
+
* rows = minutes of serial D1 writes at demo latency): every row
|
|
2375
|
+
* lands exactly as putEntity would land it (the upsert + its
|
|
2376
|
+
* journal 'persist' entry, one journal row per input row, a
|
|
2377
|
+
* duplicate id re-journals and the last write wins), but the rows
|
|
2378
|
+
* ride ONE db.batch per PUT_ENTITIES_CHUNK rows instead of one
|
|
2379
|
+
* round trip per row.
|
|
2380
|
+
*
|
|
2381
|
+
* The ordering contract — a semantic property of the audit chain,
|
|
2382
|
+
* never an implementation detail: the journal's seq order IS the
|
|
2383
|
+
* input's row order. The statements ride each batch in input order
|
|
2384
|
+
* and the chunks issue SERIALLY (parallel chunks would interleave
|
|
2385
|
+
* the seq assignment); a caller parallelizing the call itself
|
|
2386
|
+
* forfeits the contract.
|
|
2387
|
+
*
|
|
2388
|
+
* The failure contract: the chunk is the ATOMIC unit (a D1 batch is
|
|
2389
|
+
* all-or-nothing; the SQLite half wraps each chunk in a
|
|
2390
|
+
* transaction). A failed chunk lands NOTHING of its rows and the
|
|
2391
|
+
* call throws — but earlier chunks' writes STAND, and later chunks
|
|
2392
|
+
* never issue. A caller needing the whole import all-or-nothing
|
|
2393
|
+
* keeps it under one chunk (or compensates above the seam; the
|
|
2394
|
+
* retry re-journals, the same caveat as any putEntity retry — the
|
|
2395
|
+
* bounded-write facade's StoreUnavailable "may have landed"
|
|
2396
|
+
* posture). An empty rows list resolves without issuing a
|
|
2397
|
+
* statement. */
|
|
2398
|
+
putEntities(store: string, rows: readonly EntityWriteInput[]): Promise<void>
|
|
2352
2399
|
deleteEntity(store: string, id: string): Promise<boolean>
|
|
2353
2400
|
changesAfter(seq: number, limit?: number): Promise<EntityChange[]>
|
|
2354
2401
|
/** The GLOBAL journal high-water: the bootstrap snapshot's ETag leg
|