@oimlsmart/platform-server 0.2.9 → 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 +110 -23
- package/src/store/sqlite/consent-grants-store.ts +9 -0
- package/src/store/sqlite/events.ts +29 -8
- package/src/store/sqlite/op-store.ts +19 -0
- package/src/store/sqlite/store.ts +39 -12
- package/src/store/sqlite.ts +23 -20
- package/src/store.ts +114 -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. */
|
|
@@ -1456,6 +1474,16 @@ export class D1ServerStore implements ServerStore {
|
|
|
1456
1474
|
return (res.meta.changes ?? 0) > 0
|
|
1457
1475
|
}
|
|
1458
1476
|
|
|
1477
|
+
/** The governance view's population read: the client's LIVE access-token
|
|
1478
|
+
* count (unexpired — the row's absence IS the revocation). */
|
|
1479
|
+
async countOidcAccessTokensForClient(clientId: string): Promise<number> {
|
|
1480
|
+
await this.ensureMembershipSupport()
|
|
1481
|
+
const row = await this.stmt(
|
|
1482
|
+
"SELECT COUNT(*) AS n FROM oidc_access_tokens WHERE client_id = ? AND datetime(expires_at) > datetime('now')", clientId,
|
|
1483
|
+
).first<{ n: number }>()
|
|
1484
|
+
return row?.n ?? 0
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1459
1487
|
private static toRefreshToken(row: Record<string, unknown>): OidcRefreshToken {
|
|
1460
1488
|
return {
|
|
1461
1489
|
token: row.token as string,
|
|
@@ -1546,6 +1574,18 @@ export class D1ServerStore implements ServerStore {
|
|
|
1546
1574
|
return res.meta.changes ?? 0
|
|
1547
1575
|
}
|
|
1548
1576
|
|
|
1577
|
+
/** The governance view's population read: the client's LIVE refresh-token
|
|
1578
|
+
* count (unconsumed AND unexpired — a revoked family is deleted
|
|
1579
|
+
* wholesale, so a live row's presence is the offline grant's standing). */
|
|
1580
|
+
async countOidcRefreshTokensForClient(clientId: string): Promise<number> {
|
|
1581
|
+
await this.ensureMembershipSupport()
|
|
1582
|
+
await this.ensureOidcRefreshTokenSupport()
|
|
1583
|
+
const row = await this.stmt(
|
|
1584
|
+
"SELECT COUNT(*) AS n FROM oidc_refresh_tokens WHERE client_id = ? AND consumed_at IS NULL AND datetime(expires_at) > datetime('now')", clientId,
|
|
1585
|
+
).first<{ n: number }>()
|
|
1586
|
+
return row?.n ?? 0
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1549
1589
|
async listOidcKeys(): Promise<OidcKeyRow[]> {
|
|
1550
1590
|
const res = await this.stmt('SELECT * FROM oidc_keys ORDER BY created_at, kid').all<Record<string, unknown>>()
|
|
1551
1591
|
return res.results.map(row => ({
|
|
@@ -1639,6 +1679,17 @@ export class D1ServerStore implements ServerStore {
|
|
|
1639
1679
|
return (res.meta.changes ?? 0) > 0
|
|
1640
1680
|
}
|
|
1641
1681
|
|
|
1682
|
+
/** The client-registry governance view's per-client read: EVERY grant row
|
|
1683
|
+
* the client holds — live AND revoked, newest first. */
|
|
1684
|
+
async listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]> {
|
|
1685
|
+
await this.ensureConsentGrantSupport()
|
|
1686
|
+
const res = await this.stmt(
|
|
1687
|
+
'SELECT * FROM oidc_consent_grants WHERE client_id = ? ORDER BY created_at DESC, rowid DESC',
|
|
1688
|
+
clientId,
|
|
1689
|
+
).all<Record<string, unknown>>()
|
|
1690
|
+
return res.results.map(D1ServerStore.toConsentGrant)
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1642
1693
|
// ── the upstream providers (TODO.identity/08) ─────────────────────
|
|
1643
1694
|
// The provider + link rows port directly (D1 is SQLite).
|
|
1644
1695
|
|
|
@@ -3390,23 +3441,10 @@ export class D1ServerStore implements ServerStore {
|
|
|
3390
3441
|
|
|
3391
3442
|
/** Register the instrument; NULL on the (certificate, serial)
|
|
3392
3443
|
* conflict (the route's honest 409). */
|
|
3393
|
-
async createInstrumentRegistration(input: {
|
|
3394
|
-
id: string
|
|
3395
|
-
certificateId: string
|
|
3396
|
-
holderOrgId: string
|
|
3397
|
-
standardId: string
|
|
3398
|
-
serialNumber: string
|
|
3399
|
-
manufactureDate?: string | null
|
|
3400
|
-
designations?: Record<string, unknown>
|
|
3401
|
-
scopeStatus: InstrumentRegistrationScopeStatus
|
|
3402
|
-
scopeDetail?: string | null
|
|
3403
|
-
registeredBy?: string | null
|
|
3404
|
-
}): Promise<InstrumentRegistration | null> {
|
|
3444
|
+
async createInstrumentRegistration(input: InstrumentRegistrationWriteInput): Promise<InstrumentRegistration | null> {
|
|
3405
3445
|
await this.ensureInstrumentRegistrationSupport()
|
|
3406
3446
|
const res = await this.stmt(
|
|
3407
|
-
|
|
3408
|
-
(id, certificate_id, holder_org_id, standard_id, serial_number, manufacture_date, designations, scope_status, scope_detail, registered_by)
|
|
3409
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3447
|
+
INSTRUMENT_REGISTRATION_INSERT_SQL,
|
|
3410
3448
|
input.id, input.certificateId, input.holderOrgId, input.standardId, input.serialNumber,
|
|
3411
3449
|
input.manufactureDate ?? null, JSON.stringify(input.designations ?? {}),
|
|
3412
3450
|
input.scopeStatus, input.scopeDetail ?? null, input.registeredBy ?? null,
|
|
@@ -3415,6 +3453,37 @@ export class D1ServerStore implements ServerStore {
|
|
|
3415
3453
|
return this.getInstrumentRegistration(input.id)
|
|
3416
3454
|
}
|
|
3417
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
|
+
|
|
3418
3487
|
/** The lifecycle act (the transition rule is the route's); stamps
|
|
3419
3488
|
* updated_at/by. NULL when the register does not carry the id. */
|
|
3420
3489
|
async setInstrumentRegistrationLifecycle(
|
|
@@ -3535,24 +3604,42 @@ export class D1ServerStore implements ServerStore {
|
|
|
3535
3604
|
}
|
|
3536
3605
|
}
|
|
3537
3606
|
|
|
3538
|
-
async appendEvent(input: {
|
|
3539
|
-
id: string
|
|
3540
|
-
domain: string
|
|
3541
|
-
entityId: string
|
|
3542
|
-
action: string
|
|
3543
|
-
payload: string
|
|
3544
|
-
}): Promise<PlatformEvent> {
|
|
3607
|
+
async appendEvent(input: EventWriteInput): Promise<PlatformEvent> {
|
|
3545
3608
|
// ONE round trip: the RETURNING clause (SQLite ≥ 3.35, D1 included)
|
|
3546
3609
|
// answers the stored row — seq + the default at — off the INSERT
|
|
3547
3610
|
// itself; the SELECT-by-id read-back retired (the 2026-09-06 audit:
|
|
3548
3611
|
// two round trips per event, halved).
|
|
3549
3612
|
const row = await this.stmt(
|
|
3550
|
-
|
|
3613
|
+
EVENT_INSERT_SQL,
|
|
3551
3614
|
input.id, input.domain, input.entityId, input.action, input.payload,
|
|
3552
3615
|
).first<Record<string, unknown>>()
|
|
3553
3616
|
return D1ServerStore.toPlatformEvent(row!)
|
|
3554
3617
|
}
|
|
3555
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
|
+
|
|
3556
3643
|
async eventsAfter(seq: number, limit = 500): Promise<PlatformEvent[]> {
|
|
3557
3644
|
const res = await this.stmt(
|
|
3558
3645
|
'SELECT * FROM events WHERE seq > ? ORDER BY seq LIMIT ?', seq, limit,
|
|
@@ -77,6 +77,15 @@ export function listConsentGrants(userId: string): OidcConsentGrant[] {
|
|
|
77
77
|
return rows.map(toConsentGrant)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** The client-registry governance view's per-client read: EVERY grant row
|
|
81
|
+
* the client holds — live AND revoked, newest first. */
|
|
82
|
+
export function listOidcConsentGrantsForClient(clientId: string): OidcConsentGrant[] {
|
|
83
|
+
const rows = getDb().prepare(
|
|
84
|
+
'SELECT * FROM oidc_consent_grants WHERE client_id = ? ORDER BY created_at DESC, rowid DESC',
|
|
85
|
+
).all(clientId) as Array<Record<string, unknown>>
|
|
86
|
+
return rows.map(toConsentGrant)
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
/** The guarded revoke: the owner's LIVE row flips, once. */
|
|
81
90
|
export function revokeConsentGrant(id: string, userId: string): boolean {
|
|
82
91
|
return getDb().prepare(
|
|
@@ -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()
|
|
@@ -275,6 +275,15 @@ export function deleteOidcAccessToken(token: string, clientId: string): boolean
|
|
|
275
275
|
return res.changes > 0
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/** The governance view's population read: the client's LIVE access-token
|
|
279
|
+
* count (unexpired — the row's absence IS the revocation). */
|
|
280
|
+
export function countOidcAccessTokensForClient(clientId: string): number {
|
|
281
|
+
const row = getDb().prepare(
|
|
282
|
+
"SELECT COUNT(*) AS n FROM oidc_access_tokens WHERE client_id = ? AND datetime(expires_at) > datetime('now')",
|
|
283
|
+
).get(clientId) as { n: number }
|
|
284
|
+
return row.n
|
|
285
|
+
}
|
|
286
|
+
|
|
278
287
|
function toOidcRefreshToken(row: Record<string, unknown>): OidcRefreshToken {
|
|
279
288
|
return {
|
|
280
289
|
token: row.token as string,
|
|
@@ -354,6 +363,16 @@ export function deleteOidcRefreshTokensForUserClient(userId: string, clientId: s
|
|
|
354
363
|
return getDb().prepare('DELETE FROM oidc_refresh_tokens WHERE user_id = ? AND client_id = ?').run(userId, clientId).changes
|
|
355
364
|
}
|
|
356
365
|
|
|
366
|
+
/** The governance view's population read: the client's LIVE refresh-token
|
|
367
|
+
* count (unconsumed AND unexpired — a revoked family is deleted wholesale,
|
|
368
|
+
* so a live row's presence is the offline grant's standing). */
|
|
369
|
+
export function countOidcRefreshTokensForClient(clientId: string): number {
|
|
370
|
+
const row = getDb().prepare(
|
|
371
|
+
"SELECT COUNT(*) AS n FROM oidc_refresh_tokens WHERE client_id = ? AND consumed_at IS NULL AND datetime(expires_at) > datetime('now')",
|
|
372
|
+
).get(clientId) as { n: number }
|
|
373
|
+
return row.n
|
|
374
|
+
}
|
|
375
|
+
|
|
357
376
|
/** The amr column's honest parse (a JSON array of strings, else null —
|
|
358
377
|
* the provenance is absent on rows that predate the wave). */
|
|
359
378
|
function parseJsonStringList(raw: unknown): string[] | null {
|
|
@@ -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,
|
|
@@ -120,6 +122,8 @@ import {
|
|
|
120
122
|
import {
|
|
121
123
|
consumeOidcCode,
|
|
122
124
|
consumeOidcRefreshToken,
|
|
125
|
+
countOidcAccessTokensForClient,
|
|
126
|
+
countOidcRefreshTokensForClient,
|
|
123
127
|
createOidcAccessToken,
|
|
124
128
|
createOidcAuthorization,
|
|
125
129
|
createOidcCode,
|
|
@@ -187,7 +191,7 @@ import {
|
|
|
187
191
|
updateOpAccount,
|
|
188
192
|
updateUserName,
|
|
189
193
|
} from './sqlite/op-accounts-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'
|
|
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'
|
|
191
195
|
import {
|
|
192
196
|
advanceWebauthnCounter,
|
|
193
197
|
consumeMfaPending,
|
|
@@ -223,6 +227,7 @@ import {
|
|
|
223
227
|
import {
|
|
224
228
|
getConsentGrant,
|
|
225
229
|
listConsentGrants,
|
|
230
|
+
listOidcConsentGrantsForClient,
|
|
226
231
|
recordConsentGrant,
|
|
227
232
|
revokeConsentGrant,
|
|
228
233
|
} from './sqlite/consent-grants-store'
|
|
@@ -568,20 +573,12 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
568
573
|
async getInstrumentRegistration(id: string): Promise<InstrumentRegistration | null> {
|
|
569
574
|
return getInstrumentRegistration(id)
|
|
570
575
|
},
|
|
571
|
-
async createInstrumentRegistration(input: {
|
|
572
|
-
id: string
|
|
573
|
-
certificateId: string
|
|
574
|
-
holderOrgId: string
|
|
575
|
-
standardId: string
|
|
576
|
-
serialNumber: string
|
|
577
|
-
manufactureDate?: string | null
|
|
578
|
-
designations?: Record<string, unknown>
|
|
579
|
-
scopeStatus: InstrumentRegistrationScopeStatus
|
|
580
|
-
scopeDetail?: string | null
|
|
581
|
-
registeredBy?: string | null
|
|
582
|
-
}): Promise<InstrumentRegistration | null> {
|
|
576
|
+
async createInstrumentRegistration(input: InstrumentRegistrationWriteInput): Promise<InstrumentRegistration | null> {
|
|
583
577
|
return createInstrumentRegistration(input)
|
|
584
578
|
},
|
|
579
|
+
async createInstrumentRegistrations(rows: readonly InstrumentRegistrationWriteInput[]): Promise<(InstrumentRegistration | null)[]> {
|
|
580
|
+
return createInstrumentRegistrations(rows)
|
|
581
|
+
},
|
|
585
582
|
async setInstrumentRegistrationLifecycle(
|
|
586
583
|
id: string,
|
|
587
584
|
lifecycle: InstrumentRegistrationLifecycle,
|
|
@@ -672,6 +669,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
672
669
|
async deleteOidcAccessToken(token: string, clientId: string): Promise<boolean> {
|
|
673
670
|
return deleteOidcAccessToken(token, clientId)
|
|
674
671
|
},
|
|
672
|
+
async countOidcAccessTokensForClient(clientId: string): Promise<number> {
|
|
673
|
+
return countOidcAccessTokensForClient(clientId)
|
|
674
|
+
},
|
|
675
675
|
async createOidcRefreshToken(input: {
|
|
676
676
|
token: string
|
|
677
677
|
userId: string
|
|
@@ -694,6 +694,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
694
694
|
async deleteOidcRefreshTokensForUserClient(userId: string, clientId: string): Promise<number> {
|
|
695
695
|
return deleteOidcRefreshTokensForUserClient(userId, clientId)
|
|
696
696
|
},
|
|
697
|
+
async countOidcRefreshTokensForClient(clientId: string): Promise<number> {
|
|
698
|
+
return countOidcRefreshTokensForClient(clientId)
|
|
699
|
+
},
|
|
697
700
|
async listOidcKeys(): Promise<OidcKeyRow[]> {
|
|
698
701
|
return listOidcKeys()
|
|
699
702
|
},
|
|
@@ -717,6 +720,9 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
717
720
|
async revokeConsentGrant(id: string, userId: string): Promise<boolean> {
|
|
718
721
|
return revokeConsentGrant(id, userId)
|
|
719
722
|
},
|
|
723
|
+
async listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]> {
|
|
724
|
+
return listOidcConsentGrantsForClient(clientId)
|
|
725
|
+
},
|
|
720
726
|
|
|
721
727
|
// ── the upstream providers (TODO.identity/08) ──
|
|
722
728
|
async listIdentityProviders(): Promise<IdentityProvider[]> {
|
|
@@ -1032,15 +1038,12 @@ export function createSqliteServerStore(): ServerStore {
|
|
|
1032
1038
|
},
|
|
1033
1039
|
|
|
1034
1040
|
// ── the platform event store (TODO.notify/01) ──
|
|
1035
|
-
async appendEvent(input: {
|
|
1036
|
-
id: string
|
|
1037
|
-
domain: string
|
|
1038
|
-
entityId: string
|
|
1039
|
-
action: string
|
|
1040
|
-
payload: string
|
|
1041
|
-
}): Promise<PlatformEvent> {
|
|
1041
|
+
async appendEvent(input: EventWriteInput): Promise<PlatformEvent> {
|
|
1042
1042
|
return appendEvent(input)
|
|
1043
1043
|
},
|
|
1044
|
+
async appendEvents(events: readonly EventWriteInput[]): Promise<PlatformEvent[]> {
|
|
1045
|
+
return appendEvents(events)
|
|
1046
|
+
},
|
|
1044
1047
|
async eventsAfter(seq: number, limit = 500): Promise<PlatformEvent[]> {
|
|
1045
1048
|
return eventsAfter(seq, limit)
|
|
1046
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
|
|
@@ -1684,6 +1735,12 @@ export interface ServerStore {
|
|
|
1684
1735
|
* client answers false). An absent row answers false too (the endpoint
|
|
1685
1736
|
* masks both behind its 200). */
|
|
1686
1737
|
deleteOidcAccessToken(token: string, clientId: string): Promise<boolean>
|
|
1738
|
+
/** The client-registry governance view's population read: the count of
|
|
1739
|
+
* the client's LIVE access tokens (unexpired — the row's absence IS the
|
|
1740
|
+
* revocation, so presence + liveness is standing). A count, never the
|
|
1741
|
+
* rows: the governance console answers "how many sessions stand behind
|
|
1742
|
+
* this client", never a token value. */
|
|
1743
|
+
countOidcAccessTokensForClient(clientId: string): Promise<number>
|
|
1687
1744
|
/** The refresh tokens (migration 0025 — the SSO wave-C token surface).
|
|
1688
1745
|
* The mint: token is the opaque value the route generated, familyId the
|
|
1689
1746
|
* rotation lineage (a fresh id at the code exchange's first mint, the
|
|
@@ -1717,6 +1774,12 @@ export interface ServerStore {
|
|
|
1717
1774
|
* (account, client) pair goes — the "Revoke access" act ends the
|
|
1718
1775
|
* offline half with the remembered consent. Answers the count. */
|
|
1719
1776
|
deleteOidcRefreshTokensForUserClient(userId: string, clientId: string): Promise<number>
|
|
1777
|
+
/** The client-registry governance view's population read: the count of
|
|
1778
|
+
* the client's LIVE refresh tokens (unconsumed AND unexpired — a
|
|
1779
|
+
* revoked family is DELETED wholesale, so a live row's presence is the
|
|
1780
|
+
* grant's standing). A count, never the rows: the console answers "how
|
|
1781
|
+
* many offline grants stand", never a token value. */
|
|
1782
|
+
countOidcRefreshTokensForClient(clientId: string): Promise<number>
|
|
1720
1783
|
/** The key rotation history (public halves). */
|
|
1721
1784
|
listOidcKeys(): Promise<OidcKeyRow[]>
|
|
1722
1785
|
upsertOidcKey(input: { kid: string; publicJwk: string }): Promise<void>
|
|
@@ -1743,6 +1806,13 @@ export interface ServerStore {
|
|
|
1743
1806
|
* live row — a second revoke or another account's row answers false
|
|
1744
1807
|
* (the PAT guard's posture). The row STAYS. */
|
|
1745
1808
|
revokeConsentGrant(id: string, userId: string): Promise<boolean>
|
|
1809
|
+
/** The client-registry governance view's per-client read: EVERY grant
|
|
1810
|
+
* row the client holds — live AND revoked (the account console's
|
|
1811
|
+
* listConsentGrants hides the revoked half; the governance view shows
|
|
1812
|
+
* the history the audit chain would otherwise carry alone), newest
|
|
1813
|
+
* first. Read-only: the acts stay on the account console and the
|
|
1814
|
+
* dashboard's revoke routes. */
|
|
1815
|
+
listOidcConsentGrantsForClient(clientId: string): Promise<OidcConsentGrant[]>
|
|
1746
1816
|
|
|
1747
1817
|
// ── the upstream providers (TODO.identity/08) ──
|
|
1748
1818
|
/** The upstream registry (admin-managed; OP_UPSTREAM_SEED bootstraps).
|
|
@@ -2338,18 +2408,35 @@ export interface ServerStore {
|
|
|
2338
2408
|
/** Register the instrument. NULL on the (certificate_id,
|
|
2339
2409
|
* serial_number) conflict — the same physical unit never registers
|
|
2340
2410
|
* twice under one certificate (the route's honest 409). */
|
|
2341
|
-
createInstrumentRegistration(input:
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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)[]>
|
|
2353
2440
|
/** The lifecycle act (registered ⇄ out_of_service → withdrawn; the
|
|
2354
2441
|
* transition RULE is the route's — withdrawn is terminal): stamps
|
|
2355
2442
|
* updated_at/by. NULL when the register does not carry the id. */
|
|
@@ -2420,13 +2507,21 @@ export interface ServerStore {
|
|
|
2420
2507
|
* acting request's envelope). Answers the stored row (seq + at read
|
|
2421
2508
|
* back) off the INSERT's own RETURNING — ONE round trip, never the
|
|
2422
2509
|
* INSERT + SELECT-by-id pair. */
|
|
2423
|
-
appendEvent(input:
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
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[]>
|
|
2430
2525
|
/** The feed's raw leg: events past the cursor, seq-ordered. The
|
|
2431
2526
|
* visibility gate is the READER's layer (server/notify-feed.ts) —
|
|
2432
2527
|
* never waived here, never duplicated into the SQL. */
|