@oimlsmart/platform-server 0.2.10 → 0.2.11
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 +77 -23
- package/src/store/sqlite/events.ts +29 -8
- package/src/store/sqlite/store.ts +39 -12
- package/src/store/sqlite.ts +11 -20
- package/src/store.ts +95 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
|
|
31
31
|
import type { D1Database, D1PreparedStatement } from '@cloudflare/workers-types'
|
|
32
32
|
import {
|
|
33
|
+
APPEND_EVENTS_CHUNK,
|
|
33
34
|
DEMO_PASSWORD,
|
|
34
35
|
EVENTS_BULK_KEY_CHUNK,
|
|
36
|
+
INSTRUMENT_REGISTRATIONS_CHUNK,
|
|
35
37
|
PUT_ENTITIES_CHUNK,
|
|
36
38
|
StoreUnavailable,
|
|
37
39
|
type AccountEmail,
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
type EntityWriteInput,
|
|
49
51
|
type EventEntityKey,
|
|
50
52
|
type EventKeyFilter,
|
|
53
|
+
type EventWriteInput,
|
|
51
54
|
type FederationPeer,
|
|
52
55
|
type IdentityApproval,
|
|
53
56
|
type IdentityLink,
|
|
@@ -84,6 +87,7 @@ import {
|
|
|
84
87
|
type InstrumentRegistration,
|
|
85
88
|
type InstrumentRegistrationLifecycle,
|
|
86
89
|
type InstrumentRegistrationScopeStatus,
|
|
90
|
+
type InstrumentRegistrationWriteInput,
|
|
87
91
|
type PersonalAccessToken,
|
|
88
92
|
type PlatformEvent,
|
|
89
93
|
resolveOrgContext,
|
|
@@ -423,6 +427,20 @@ const ENTITY_UPSERT_SQL = `INSERT INTO entities (store, id, org_id, data, update
|
|
|
423
427
|
ON CONFLICT (store, id) DO UPDATE SET org_id = excluded.org_id, data = excluded.data, updated_at = datetime('now')`
|
|
424
428
|
const ENTITY_CHANGE_SQL = 'INSERT INTO entity_changes (store, type, id) VALUES (?, ?, ?)'
|
|
425
429
|
|
|
430
|
+
/** The register write's INSERT — ONE textual source for
|
|
431
|
+
* createInstrumentRegistration and createInstrumentRegistrations alike
|
|
432
|
+
* (the batch verb appends RETURNING * so the stored row answers off
|
|
433
|
+
* the write itself; the single verb keeps its .run() + by-id
|
|
434
|
+
* read-back pair). */
|
|
435
|
+
const INSTRUMENT_REGISTRATION_INSERT_SQL = `INSERT OR IGNORE INTO instrument_registrations
|
|
436
|
+
(id, certificate_id, holder_org_id, standard_id, serial_number, manufacture_date, designations, scope_status, scope_detail, registered_by)
|
|
437
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
438
|
+
|
|
439
|
+
/** The event append's INSERT … RETURNING * — ONE textual source for
|
|
440
|
+
* appendEvent and appendEvents alike (the stored row — seq + the
|
|
441
|
+
* default at — answers off the write itself). */
|
|
442
|
+
const EVENT_INSERT_SQL = 'INSERT INTO events (id, domain, entity_id, action, payload) VALUES (?, ?, ?, ?, ?) RETURNING *'
|
|
443
|
+
|
|
426
444
|
export class D1ServerStore implements ServerStore {
|
|
427
445
|
/** The RAW binding — the ensure memos (and d1StoreFor's map) key on
|
|
428
446
|
* it: the facade below is per-instance and would never hit. */
|
|
@@ -3423,23 +3441,10 @@ export class D1ServerStore implements ServerStore {
|
|
|
3423
3441
|
|
|
3424
3442
|
/** Register the instrument; NULL on the (certificate, serial)
|
|
3425
3443
|
* conflict (the route's honest 409). */
|
|
3426
|
-
async createInstrumentRegistration(input: {
|
|
3427
|
-
id: string
|
|
3428
|
-
certificateId: string
|
|
3429
|
-
holderOrgId: string
|
|
3430
|
-
standardId: string
|
|
3431
|
-
serialNumber: string
|
|
3432
|
-
manufactureDate?: string | null
|
|
3433
|
-
designations?: Record<string, unknown>
|
|
3434
|
-
scopeStatus: InstrumentRegistrationScopeStatus
|
|
3435
|
-
scopeDetail?: string | null
|
|
3436
|
-
registeredBy?: string | null
|
|
3437
|
-
}): Promise<InstrumentRegistration | null> {
|
|
3444
|
+
async createInstrumentRegistration(input: InstrumentRegistrationWriteInput): Promise<InstrumentRegistration | null> {
|
|
3438
3445
|
await this.ensureInstrumentRegistrationSupport()
|
|
3439
3446
|
const res = await this.stmt(
|
|
3440
|
-
|
|
3441
|
-
(id, certificate_id, holder_org_id, standard_id, serial_number, manufacture_date, designations, scope_status, scope_detail, registered_by)
|
|
3442
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3447
|
+
INSTRUMENT_REGISTRATION_INSERT_SQL,
|
|
3443
3448
|
input.id, input.certificateId, input.holderOrgId, input.standardId, input.serialNumber,
|
|
3444
3449
|
input.manufactureDate ?? null, JSON.stringify(input.designations ?? {}),
|
|
3445
3450
|
input.scopeStatus, input.scopeDetail ?? null, input.registeredBy ?? null,
|
|
@@ -3448,6 +3453,37 @@ export class D1ServerStore implements ServerStore {
|
|
|
3448
3453
|
return this.getInstrumentRegistration(input.id)
|
|
3449
3454
|
}
|
|
3450
3455
|
|
|
3456
|
+
/** The batch register write (the seam's contract, mechanically):
|
|
3457
|
+
* each row's INSERT OR IGNORE … RETURNING * rides ONE db.batch per
|
|
3458
|
+
* INSTRUMENT_REGISTRATIONS_CHUNK rows, the statements in INPUT order,
|
|
3459
|
+
* the chunks SERIALLY — the register's insertion order IS the CSV's
|
|
3460
|
+
* row order. The per-row answer comes off the batch's own results:
|
|
3461
|
+
* the stored row, or NULL when the RETURNING came back empty (the
|
|
3462
|
+
* (certificate, serial) conflict — the single verb's honest null).
|
|
3463
|
+
* The chunk is the atomic unit: a D1 batch lands all-or-nothing; a
|
|
3464
|
+
* failed chunk throws with its rows unlanded, earlier chunks
|
|
3465
|
+
* standing, later chunks never issued. */
|
|
3466
|
+
async createInstrumentRegistrations(rows: readonly InstrumentRegistrationWriteInput[]): Promise<(InstrumentRegistration | null)[]> {
|
|
3467
|
+
await this.ensureInstrumentRegistrationSupport()
|
|
3468
|
+
if (rows.length === 0) return []
|
|
3469
|
+
const out: (InstrumentRegistration | null)[] = []
|
|
3470
|
+
for (let i = 0; i < rows.length; i += INSTRUMENT_REGISTRATIONS_CHUNK) {
|
|
3471
|
+
const chunk = rows.slice(i, i + INSTRUMENT_REGISTRATIONS_CHUNK)
|
|
3472
|
+
const statements = chunk.map(row => this.stmt(
|
|
3473
|
+
`${INSTRUMENT_REGISTRATION_INSERT_SQL} RETURNING *`,
|
|
3474
|
+
row.id, row.certificateId, row.holderOrgId, row.standardId, row.serialNumber,
|
|
3475
|
+
row.manufactureDate ?? null, JSON.stringify(row.designations ?? {}),
|
|
3476
|
+
row.scopeStatus, row.scopeDetail ?? null, row.registeredBy ?? null,
|
|
3477
|
+
))
|
|
3478
|
+
const results = await this.db.batch(statements)
|
|
3479
|
+
for (const res of results) {
|
|
3480
|
+
const stored = res.results[0] as Record<string, unknown> | undefined
|
|
3481
|
+
out.push(stored ? D1ServerStore.toInstrumentRegistration(stored) : null)
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
return out
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3451
3487
|
/** The lifecycle act (the transition rule is the route's); stamps
|
|
3452
3488
|
* updated_at/by. NULL when the register does not carry the id. */
|
|
3453
3489
|
async setInstrumentRegistrationLifecycle(
|
|
@@ -3568,24 +3604,42 @@ export class D1ServerStore implements ServerStore {
|
|
|
3568
3604
|
}
|
|
3569
3605
|
}
|
|
3570
3606
|
|
|
3571
|
-
async appendEvent(input: {
|
|
3572
|
-
id: string
|
|
3573
|
-
domain: string
|
|
3574
|
-
entityId: string
|
|
3575
|
-
action: string
|
|
3576
|
-
payload: string
|
|
3577
|
-
}): Promise<PlatformEvent> {
|
|
3607
|
+
async appendEvent(input: EventWriteInput): Promise<PlatformEvent> {
|
|
3578
3608
|
// ONE round trip: the RETURNING clause (SQLite ≥ 3.35, D1 included)
|
|
3579
3609
|
// answers the stored row — seq + the default at — off the INSERT
|
|
3580
3610
|
// itself; the SELECT-by-id read-back retired (the 2026-09-06 audit:
|
|
3581
3611
|
// two round trips per event, halved).
|
|
3582
3612
|
const row = await this.stmt(
|
|
3583
|
-
|
|
3613
|
+
EVENT_INSERT_SQL,
|
|
3584
3614
|
input.id, input.domain, input.entityId, input.action, input.payload,
|
|
3585
3615
|
).first<Record<string, unknown>>()
|
|
3586
3616
|
return D1ServerStore.toPlatformEvent(row!)
|
|
3587
3617
|
}
|
|
3588
3618
|
|
|
3619
|
+
/** The bulk append (the seam's contract, mechanically): each event's
|
|
3620
|
+
* INSERT … RETURNING * rides ONE db.batch per APPEND_EVENTS_CHUNK
|
|
3621
|
+
* rows, the statements in INPUT order, the chunks SERIALLY — the
|
|
3622
|
+
* events' seq order IS the input order. The answer is the stored
|
|
3623
|
+
* rows, input-aligned. The chunk is the atomic unit: a D1 batch
|
|
3624
|
+
* lands all-or-nothing; a failed chunk throws with its events
|
|
3625
|
+
* unlanded, earlier chunks standing, later chunks never issued. */
|
|
3626
|
+
async appendEvents(events: readonly EventWriteInput[]): Promise<PlatformEvent[]> {
|
|
3627
|
+
if (events.length === 0) return []
|
|
3628
|
+
const out: PlatformEvent[] = []
|
|
3629
|
+
for (let i = 0; i < events.length; i += APPEND_EVENTS_CHUNK) {
|
|
3630
|
+
const chunk = events.slice(i, i + APPEND_EVENTS_CHUNK)
|
|
3631
|
+
const statements = chunk.map(e => this.stmt(
|
|
3632
|
+
EVENT_INSERT_SQL,
|
|
3633
|
+
e.id, e.domain, e.entityId, e.action, e.payload,
|
|
3634
|
+
))
|
|
3635
|
+
const results = await this.db.batch(statements)
|
|
3636
|
+
for (const res of results) {
|
|
3637
|
+
out.push(D1ServerStore.toPlatformEvent(res.results[0] as Record<string, unknown>))
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return out
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3589
3643
|
async eventsAfter(seq: number, limit = 500): Promise<PlatformEvent[]> {
|
|
3590
3644
|
const res = await this.stmt(
|
|
3591
3645
|
'SELECT * FROM events WHERE seq > ? ORDER BY seq LIMIT ?', seq, limit,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// ═══════════════════════════════════════════════════════════════════
|
|
10
10
|
|
|
11
11
|
import { getDb } from './store'
|
|
12
|
-
import { EVENTS_BULK_KEY_CHUNK, type EventEntityKey, type EventKeyFilter, type PlatformEvent } from '../../store'
|
|
12
|
+
import { APPEND_EVENTS_CHUNK, EVENTS_BULK_KEY_CHUNK, type EventEntityKey, type EventKeyFilter, type EventWriteInput, type PlatformEvent } from '../../store'
|
|
13
13
|
|
|
14
14
|
interface EventRow {
|
|
15
15
|
seq: number
|
|
@@ -33,13 +33,7 @@ function toPlatformEvent(row: EventRow): PlatformEvent {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function appendEvent(input: {
|
|
37
|
-
id: string
|
|
38
|
-
domain: string
|
|
39
|
-
entityId: string
|
|
40
|
-
action: string
|
|
41
|
-
payload: string
|
|
42
|
-
}): PlatformEvent {
|
|
36
|
+
export function appendEvent(input: EventWriteInput): PlatformEvent {
|
|
43
37
|
// ONE statement: RETURNING answers the stored row (seq + the default
|
|
44
38
|
// at) off the INSERT itself — the same halving as the D1 half.
|
|
45
39
|
return toPlatformEvent(
|
|
@@ -49,6 +43,33 @@ export function appendEvent(input: {
|
|
|
49
43
|
)
|
|
50
44
|
}
|
|
51
45
|
|
|
46
|
+
/** The bulk append (the seam's appendEvents, the 2026-09-07 audit's
|
|
47
|
+
* chain half): each event lands exactly as appendEvent would land it —
|
|
48
|
+
* the INSERT … RETURNING * answers the stored row off the write itself —
|
|
49
|
+
* the answer rows in INPUT order (their seqs strictly increase in it),
|
|
50
|
+
* one transaction per APPEND_EVENTS_CHUNK rows. The chunk is the atomic
|
|
51
|
+
* unit, matching the D1 batch's all-or-nothing; a failed chunk throws
|
|
52
|
+
* with its events unlanded, earlier chunks standing, later chunks never
|
|
53
|
+
* issued. The chunks run serially, so the events' seq order IS the
|
|
54
|
+
* input order. */
|
|
55
|
+
export function appendEvents(events: readonly EventWriteInput[]): PlatformEvent[] {
|
|
56
|
+
if (events.length === 0) return []
|
|
57
|
+
const db = getDb()
|
|
58
|
+
const insert = db.prepare(
|
|
59
|
+
`INSERT INTO events (id, domain, entity_id, action, payload) VALUES (?, ?, ?, ?, ?) RETURNING *`,
|
|
60
|
+
)
|
|
61
|
+
const out: PlatformEvent[] = []
|
|
62
|
+
for (let i = 0; i < events.length; i += APPEND_EVENTS_CHUNK) {
|
|
63
|
+
const chunk = events.slice(i, i + APPEND_EVENTS_CHUNK)
|
|
64
|
+
db.transaction(() => {
|
|
65
|
+
for (const e of chunk) {
|
|
66
|
+
out.push(toPlatformEvent(insert.get(e.id, e.domain, e.entityId, e.action, e.payload) as EventRow))
|
|
67
|
+
}
|
|
68
|
+
})()
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
/** The feed's raw leg: events past the cursor, seq-ordered. */
|
|
53
74
|
export function eventsAfter(seq: number, limit = 500): PlatformEvent[] {
|
|
54
75
|
const rows = getDb()
|
|
@@ -1355,7 +1355,9 @@ import type {
|
|
|
1355
1355
|
InstrumentRegistration,
|
|
1356
1356
|
InstrumentRegistrationLifecycle,
|
|
1357
1357
|
InstrumentRegistrationScopeStatus,
|
|
1358
|
+
InstrumentRegistrationWriteInput,
|
|
1358
1359
|
} from '../../store'
|
|
1360
|
+
import { INSTRUMENT_REGISTRATIONS_CHUNK } from '../../store'
|
|
1359
1361
|
|
|
1360
1362
|
interface InstrumentRegistrationRow {
|
|
1361
1363
|
id: string
|
|
@@ -1430,18 +1432,7 @@ export function getInstrumentRegistration(id: string): InstrumentRegistration |
|
|
|
1430
1432
|
/** Register the instrument; NULL on the (certificate, serial) conflict
|
|
1431
1433
|
* (the same physical unit never registers twice under one certificate —
|
|
1432
1434
|
* the route's honest 409). */
|
|
1433
|
-
export function createInstrumentRegistration(input: {
|
|
1434
|
-
id: string
|
|
1435
|
-
certificateId: string
|
|
1436
|
-
holderOrgId: string
|
|
1437
|
-
standardId: string
|
|
1438
|
-
serialNumber: string
|
|
1439
|
-
manufactureDate?: string | null
|
|
1440
|
-
designations?: Record<string, unknown>
|
|
1441
|
-
scopeStatus: InstrumentRegistrationScopeStatus
|
|
1442
|
-
scopeDetail?: string | null
|
|
1443
|
-
registeredBy?: string | null
|
|
1444
|
-
}): InstrumentRegistration | null {
|
|
1435
|
+
export function createInstrumentRegistration(input: InstrumentRegistrationWriteInput): InstrumentRegistration | null {
|
|
1445
1436
|
const res = getDb().prepare(
|
|
1446
1437
|
`INSERT OR IGNORE INTO instrument_registrations
|
|
1447
1438
|
(id, certificate_id, holder_org_id, standard_id, serial_number, manufacture_date, designations, scope_status, scope_detail, registered_by)
|
|
@@ -1455,6 +1446,42 @@ export function createInstrumentRegistration(input: {
|
|
|
1455
1446
|
return getInstrumentRegistration(input.id)
|
|
1456
1447
|
}
|
|
1457
1448
|
|
|
1449
|
+
/** The batch register write (the seam's createInstrumentRegistrations,
|
|
1450
|
+
* the 2026-09-07 audit's REAL J1): each row lands exactly as
|
|
1451
|
+
* createInstrumentRegistration would land it — the INSERT OR IGNORE …
|
|
1452
|
+
* RETURNING * answers the stored row off the write itself (NULL when it
|
|
1453
|
+
* comes back empty: the (certificate, serial) conflict), the per-row
|
|
1454
|
+
* answers aligned with the INPUT order — one transaction per
|
|
1455
|
+
* INSTRUMENT_REGISTRATIONS_CHUNK rows. The chunk is the atomic unit,
|
|
1456
|
+
* matching the D1 batch's all-or-nothing; a failed chunk throws with
|
|
1457
|
+
* its rows unlanded, earlier chunks standing, later chunks never
|
|
1458
|
+
* issued. The chunks run serially, so the register's insertion order
|
|
1459
|
+
* IS the input's row order. */
|
|
1460
|
+
export function createInstrumentRegistrations(rows: readonly InstrumentRegistrationWriteInput[]): (InstrumentRegistration | null)[] {
|
|
1461
|
+
if (rows.length === 0) return []
|
|
1462
|
+
const db = getDb()
|
|
1463
|
+
const insert = db.prepare(
|
|
1464
|
+
`INSERT OR IGNORE INTO instrument_registrations
|
|
1465
|
+
(id, certificate_id, holder_org_id, standard_id, serial_number, manufacture_date, designations, scope_status, scope_detail, registered_by)
|
|
1466
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1467
|
+
)
|
|
1468
|
+
const out: (InstrumentRegistration | null)[] = []
|
|
1469
|
+
for (let i = 0; i < rows.length; i += INSTRUMENT_REGISTRATIONS_CHUNK) {
|
|
1470
|
+
const chunk = rows.slice(i, i + INSTRUMENT_REGISTRATIONS_CHUNK)
|
|
1471
|
+
db.transaction(() => {
|
|
1472
|
+
for (const row of chunk) {
|
|
1473
|
+
const stored = insert.get(
|
|
1474
|
+
row.id, row.certificateId, row.holderOrgId, row.standardId, row.serialNumber,
|
|
1475
|
+
row.manufactureDate ?? null, JSON.stringify(row.designations ?? {}),
|
|
1476
|
+
row.scopeStatus, row.scopeDetail ?? null, row.registeredBy ?? null,
|
|
1477
|
+
) as InstrumentRegistrationRow | undefined
|
|
1478
|
+
out.push(stored ? instrumentRegistrationPayload(stored) : null)
|
|
1479
|
+
}
|
|
1480
|
+
})()
|
|
1481
|
+
}
|
|
1482
|
+
return out
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1458
1485
|
/** The lifecycle act (the transition rule is the route's); stamps
|
|
1459
1486
|
* updated_at/by. NULL when the register does not carry the id. */
|
|
1460
1487
|
export function setInstrumentRegistrationLifecycle(
|
package/src/store/sqlite.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
createOrgMembership,
|
|
23
23
|
createOrgRegistryOrg,
|
|
24
24
|
createInstrumentRegistration,
|
|
25
|
+
createInstrumentRegistrations,
|
|
25
26
|
createSession,
|
|
26
27
|
decideCertificateHolderClaim,
|
|
27
28
|
decideIdentityApproval,
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
91
92
|
} from './sqlite/entities'
|
|
92
93
|
import {
|
|
93
94
|
appendEvent,
|
|
95
|
+
appendEvents,
|
|
94
96
|
eventsAfter,
|
|
95
97
|
eventsMatching,
|
|
96
98
|
getEvent,
|
|
@@ -189,7 +191,7 @@ import {
|
|
|
189
191
|
updateOpAccount,
|
|
190
192
|
updateUserName,
|
|
191
193
|
} from './sqlite/op-accounts-store'
|
|
192
|
-
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'
|
|
194
|
+
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 EventWriteInput, type FederationPeer, type IdentityApproval, type IdentityLink, type IdentityProvider, type InstrumentRegistration, type InstrumentRegistrationLifecycle, type InstrumentRegistrationScopeStatus, type InstrumentRegistrationWriteInput, 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'
|
|
193
195
|
import {
|
|
194
196
|
advanceWebauthnCounter,
|
|
195
197
|
consumeMfaPending,
|
|
@@ -571,20 +573,12 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
571
573
|
async getInstrumentRegistration(id: string): Promise<InstrumentRegistration | null> {
|
|
572
574
|
return getInstrumentRegistration(id)
|
|
573
575
|
},
|
|
574
|
-
async createInstrumentRegistration(input: {
|
|
575
|
-
id: string
|
|
576
|
-
certificateId: string
|
|
577
|
-
holderOrgId: string
|
|
578
|
-
standardId: string
|
|
579
|
-
serialNumber: string
|
|
580
|
-
manufactureDate?: string | null
|
|
581
|
-
designations?: Record<string, unknown>
|
|
582
|
-
scopeStatus: InstrumentRegistrationScopeStatus
|
|
583
|
-
scopeDetail?: string | null
|
|
584
|
-
registeredBy?: string | null
|
|
585
|
-
}): Promise<InstrumentRegistration | null> {
|
|
576
|
+
async createInstrumentRegistration(input: InstrumentRegistrationWriteInput): Promise<InstrumentRegistration | null> {
|
|
586
577
|
return createInstrumentRegistration(input)
|
|
587
578
|
},
|
|
579
|
+
async createInstrumentRegistrations(rows: readonly InstrumentRegistrationWriteInput[]): Promise<(InstrumentRegistration | null)[]> {
|
|
580
|
+
return createInstrumentRegistrations(rows)
|
|
581
|
+
},
|
|
588
582
|
async setInstrumentRegistrationLifecycle(
|
|
589
583
|
id: string,
|
|
590
584
|
lifecycle: InstrumentRegistrationLifecycle,
|
|
@@ -1044,15 +1038,12 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
1044
1038
|
},
|
|
1045
1039
|
|
|
1046
1040
|
// ── the platform event store (TODO.notify/01) ──
|
|
1047
|
-
async appendEvent(input: {
|
|
1048
|
-
id: string
|
|
1049
|
-
domain: string
|
|
1050
|
-
entityId: string
|
|
1051
|
-
action: string
|
|
1052
|
-
payload: string
|
|
1053
|
-
}): Promise<PlatformEvent> {
|
|
1041
|
+
async appendEvent(input: EventWriteInput): Promise<PlatformEvent> {
|
|
1054
1042
|
return appendEvent(input)
|
|
1055
1043
|
},
|
|
1044
|
+
async appendEvents(events: readonly EventWriteInput[]): Promise<PlatformEvent[]> {
|
|
1045
|
+
return appendEvents(events)
|
|
1046
|
+
},
|
|
1056
1047
|
async eventsAfter(seq: number, limit = 500): Promise<PlatformEvent[]> {
|
|
1057
1048
|
return eventsAfter(seq, limit)
|
|
1058
1049
|
},
|
package/src/store.ts
CHANGED
|
@@ -217,6 +217,17 @@ export interface PlatformEvent {
|
|
|
217
217
|
at: string
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/** One row of the bulk append (appendEvents): appendEvent's legs — the
|
|
221
|
+
* same fields, so each event lands exactly as the single-row verb
|
|
222
|
+
* would land it (the EntityWriteInput pattern). */
|
|
223
|
+
export interface EventWriteInput {
|
|
224
|
+
id: string
|
|
225
|
+
domain: string
|
|
226
|
+
entityId: string
|
|
227
|
+
action: string
|
|
228
|
+
payload: string
|
|
229
|
+
}
|
|
230
|
+
|
|
220
231
|
/** The key pattern's SQL legs: a subscription pattern (`application/**`,
|
|
221
232
|
* `certificate` + `issued` across the domain, one entity's
|
|
222
233
|
* `test-run/asg-…-001/**`) compiles to the columns it pins; absent legs
|
|
@@ -243,6 +254,17 @@ export interface EventEntityKey {
|
|
|
243
254
|
* backends stay answer-identical. */
|
|
244
255
|
export const EVENTS_BULK_KEY_CHUNK = 49
|
|
245
256
|
|
|
257
|
+
/** The bulk append's chunk, in ROWS (appendEvents): each event
|
|
258
|
+
* contributes ONE statement (the INSERT … RETURNING * answers the
|
|
259
|
+
* stored row off the write itself, the appendEvent halving), so a
|
|
260
|
+
* chunk is one db.batch of APPEND_EVENTS_CHUNK statements — the
|
|
261
|
+
* INSTRUMENT_REGISTRATIONS_CHUNK order. The chunk bounds the ATOMIC
|
|
262
|
+
* unit (a D1 batch is all-or-nothing; the SQLite half's per-chunk
|
|
263
|
+
* transaction matches it). Chunks issue SERIALLY, in input order —
|
|
264
|
+
* the events' seq order IS the input order, and parallel chunks would
|
|
265
|
+
* forfeit it. */
|
|
266
|
+
export const APPEND_EVENTS_CHUNK = 50
|
|
267
|
+
|
|
246
268
|
// ── the notification subscriptions store (TODO.notify/02) ────────────
|
|
247
269
|
|
|
248
270
|
/** The rule row's mode: 'subscribe' adds the user to the candidates of
|
|
@@ -1339,6 +1361,35 @@ export interface InstrumentRegistration {
|
|
|
1339
1361
|
updatedBy: string | null
|
|
1340
1362
|
}
|
|
1341
1363
|
|
|
1364
|
+
/** One row of the batch register write (createInstrumentRegistrations):
|
|
1365
|
+
* createInstrumentRegistration's legs — the same fields, so each row
|
|
1366
|
+
* lands exactly as the single-row verb would land it (the
|
|
1367
|
+
* EntityWriteInput pattern). */
|
|
1368
|
+
export interface InstrumentRegistrationWriteInput {
|
|
1369
|
+
id: string
|
|
1370
|
+
certificateId: string
|
|
1371
|
+
holderOrgId: string
|
|
1372
|
+
standardId: string
|
|
1373
|
+
serialNumber: string
|
|
1374
|
+
manufactureDate?: string | null
|
|
1375
|
+
designations?: Record<string, unknown>
|
|
1376
|
+
scopeStatus: InstrumentRegistrationScopeStatus
|
|
1377
|
+
scopeDetail?: string | null
|
|
1378
|
+
registeredBy?: string | null
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/** The batch register write's chunk, in ROWS (createInstrumentRegistrations):
|
|
1382
|
+
* each row contributes ONE statement (the INSERT OR IGNORE … RETURNING *
|
|
1383
|
+
* answers the stored row off the write itself), so a chunk is one
|
|
1384
|
+
* db.batch of INSTRUMENT_REGISTRATIONS_CHUNK statements — half the
|
|
1385
|
+
* PUT_ENTITIES_CHUNK statement load, the same conservative order. The
|
|
1386
|
+
* chunk bounds the ATOMIC unit (a D1 batch is all-or-nothing; the
|
|
1387
|
+
* SQLite half's per-chunk transaction matches it). Chunks issue
|
|
1388
|
+
* SERIALLY, in input order — the register's insertion order IS the
|
|
1389
|
+
* input's row order (the CSV commit's chain events sequence after it,
|
|
1390
|
+
* same order), and parallel chunks would forfeit it. */
|
|
1391
|
+
export const INSTRUMENT_REGISTRATIONS_CHUNK = 50
|
|
1392
|
+
|
|
1342
1393
|
/** Per-store org fields for the READ visibility (the multi-party
|
|
1343
1394
|
* model: a row is visible when ANY named field equals the user's
|
|
1344
1395
|
* org). The field names are the entities' REAL ones (verified against
|
|
@@ -2357,18 +2408,35 @@ export interface ServerStore {
|
|
|
2357
2408
|
/** Register the instrument. NULL on the (certificate_id,
|
|
2358
2409
|
* serial_number) conflict — the same physical unit never registers
|
|
2359
2410
|
* twice under one certificate (the route's honest 409). */
|
|
2360
|
-
createInstrumentRegistration(input:
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2411
|
+
createInstrumentRegistration(input: InstrumentRegistrationWriteInput): Promise<InstrumentRegistration | null>
|
|
2412
|
+
/** The BATCH register write (the 2026-09-07 performance audit's REAL
|
|
2413
|
+
* J1 — the CSV commit awaited createInstrumentRegistration per
|
|
2414
|
+
* imported row: ~10⁴ rows = minutes of serial D1 writes at demo
|
|
2415
|
+
* latency, two round trips each): every row lands exactly as the
|
|
2416
|
+
* single-row verb would land it, but the rows ride ONE db.batch per
|
|
2417
|
+
* INSTRUMENT_REGISTRATIONS_CHUNK rows, the INSERT OR IGNORE …
|
|
2418
|
+
* RETURNING * answering the stored row off the write itself (the
|
|
2419
|
+
* appendEvent halving — never the INSERT + SELECT-by-id pair).
|
|
2420
|
+
*
|
|
2421
|
+
* The per-row answer, aligned with the INPUT order: the stored row,
|
|
2422
|
+
* or NULL on the (certificate_id, serial_number) conflict — the
|
|
2423
|
+
* single verb's honest null, so the route's per-row refusal leg (the
|
|
2424
|
+
* serial registered between the evaluation and the commit) rides
|
|
2425
|
+
* unchanged.
|
|
2426
|
+
*
|
|
2427
|
+
* The ordering contract: the statements ride each batch in input
|
|
2428
|
+
* order and the chunks issue SERIALLY, so the register's insertion
|
|
2429
|
+
* order IS the input's row order (the CSV row order; the commit's
|
|
2430
|
+
* chain events sequence after it, same order). A caller
|
|
2431
|
+
* parallelizing the call itself forfeits the contract.
|
|
2432
|
+
*
|
|
2433
|
+
* The failure contract mirrors putEntities: the chunk is the ATOMIC
|
|
2434
|
+
* unit (a D1 batch is all-or-nothing; the SQLite half wraps each
|
|
2435
|
+
* chunk in a transaction). A failed chunk lands NOTHING of its rows
|
|
2436
|
+
* and the call throws — earlier chunks' writes STAND, later chunks
|
|
2437
|
+
* never issue. An empty rows list resolves to [] without issuing a
|
|
2438
|
+
* statement. */
|
|
2439
|
+
createInstrumentRegistrations(rows: readonly InstrumentRegistrationWriteInput[]): Promise<(InstrumentRegistration | null)[]>
|
|
2372
2440
|
/** The lifecycle act (registered ⇄ out_of_service → withdrawn; the
|
|
2373
2441
|
* transition RULE is the route's — withdrawn is terminal): stamps
|
|
2374
2442
|
* updated_at/by. NULL when the register does not carry the id. */
|
|
@@ -2439,13 +2507,21 @@ export interface ServerStore {
|
|
|
2439
2507
|
* acting request's envelope). Answers the stored row (seq + at read
|
|
2440
2508
|
* back) off the INSERT's own RETURNING — ONE round trip, never the
|
|
2441
2509
|
* INSERT + SELECT-by-id pair. */
|
|
2442
|
-
appendEvent(input:
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2510
|
+
appendEvent(input: EventWriteInput): Promise<PlatformEvent>
|
|
2511
|
+
/** The BULK append (the 2026-09-07 performance audit's chain half —
|
|
2512
|
+
* the CSV registration commit's one chain event per imported serial,
|
|
2513
|
+
* a second serial loop after the register writes): every event lands
|
|
2514
|
+
* exactly as appendEvent would land it, the rows riding ONE
|
|
2515
|
+
* db.batch per APPEND_EVENTS_CHUNK rows. Answers the stored rows in
|
|
2516
|
+
* INPUT order — the seqs strictly increase in it (the statements
|
|
2517
|
+
* ride each batch in input order, the chunks SERIALLY; the same
|
|
2518
|
+
* semantic contract as putEntities').
|
|
2519
|
+
*
|
|
2520
|
+
* The failure contract mirrors putEntities: the chunk is the ATOMIC
|
|
2521
|
+
* unit; a failed chunk lands NOTHING of its events and the call
|
|
2522
|
+
* throws — earlier chunks stand, later chunks never issue. An empty
|
|
2523
|
+
* list resolves to [] without issuing a statement. */
|
|
2524
|
+
appendEvents(events: readonly EventWriteInput[]): Promise<PlatformEvent[]>
|
|
2449
2525
|
/** The feed's raw leg: events past the cursor, seq-ordered. The
|
|
2450
2526
|
* visibility gate is the READER's layer (server/notify-feed.ts) —
|
|
2451
2527
|
* never waived here, never duplicated into the SQL. */
|