@cosmicdrift/kumiko-framework 0.48.1 → 0.51.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/package.json +2 -2
- package/src/api/__tests__/origin-middleware.integration.test.ts +132 -0
- package/src/api/__tests__/origin-middleware.test.ts +191 -0
- package/src/api/anonymous-cookie.ts +1 -0
- package/src/api/api-constants.ts +9 -0
- package/src/api/auth-middleware.ts +2 -0
- package/src/api/auth-routes.ts +29 -4
- package/src/api/csrf-middleware.ts +1 -4
- package/src/api/origin-middleware.ts +98 -0
- package/src/api/server.ts +20 -0
- package/src/bun-db/__tests__/_helpers.ts +1 -0
- package/src/bun-db/query.ts +1 -0
- package/src/db/__tests__/migrate-generator.test.ts +11 -0
- package/src/db/collect-table-metas.ts +2 -1
- package/src/db/dialect.ts +3 -1
- package/src/db/migrate-generator.ts +6 -2
- package/src/db/queries/event-store.ts +1 -0
- package/src/engine/__tests__/boot-validator.test.ts +43 -0
- package/src/engine/__tests__/build-app-schema-settings-hub.test.ts +118 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +174 -0
- package/src/engine/__tests__/config-helpers.test.ts +57 -0
- package/src/engine/boot-validator/config-deps.ts +24 -0
- package/src/engine/boot-validator/index.ts +11 -4
- package/src/engine/build-app-schema.ts +45 -0
- package/src/engine/build-config-feature-schema.ts +228 -0
- package/src/engine/config-helpers.ts +31 -0
- package/src/engine/define-handler.ts +1 -0
- package/src/engine/entity-handlers.ts +7 -1
- package/src/engine/feature-manifest.ts +6 -7
- package/src/engine/index.ts +9 -0
- package/src/engine/pipeline.ts +1 -0
- package/src/engine/qualified-name.ts +1 -0
- package/src/engine/state-machine.ts +1 -0
- package/src/engine/steps/compute.ts +1 -1
- package/src/engine/steps/return.ts +1 -0
- package/src/engine/types/config.ts +65 -1
- package/src/engine/types/index.ts +3 -0
- package/src/engine/types/screen.ts +13 -0
- package/src/env/index.ts +1 -0
- package/src/errors/write-error-info.ts +2 -0
- package/src/es-ops/__tests__/context.integration.test.ts +33 -24
- package/src/es-ops/__tests__/runner.integration.test.ts +130 -86
- package/src/es-ops/context.ts +6 -4
- package/src/event-store/event-store.ts +3 -0
- package/src/files/file-handle.ts +1 -1
- package/src/jobs/job-runner.ts +4 -0
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +84 -1
- package/src/migrations/index.ts +5 -0
- package/src/migrations/pending-rebuilds.ts +84 -10
- package/src/pipeline/dispatcher.ts +15 -7
- package/src/pipeline/lifecycle-pipeline.ts +4 -4
- package/src/stack/table-helpers.ts +1 -0
- package/src/time/tz-context.ts +3 -3
- package/src/utils/compare.ts +10 -0
- package/src/utils/ids.ts +1 -0
- package/src/utils/index.ts +1 -0
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
unsafePushTables,
|
|
31
31
|
} from "../../stack";
|
|
32
32
|
import {
|
|
33
|
+
enqueueProjectionRebuild,
|
|
33
34
|
listPendingRebuilds,
|
|
34
35
|
queueRebuildsFromMarkers,
|
|
35
36
|
runPendingRebuilds,
|
|
@@ -162,6 +163,88 @@ describe("pending-rebuilds queue", () => {
|
|
|
162
163
|
|
|
163
164
|
test("no markers, no queue → noop", async () => {
|
|
164
165
|
const run = await runPendingRebuilds(testDb.db, registry);
|
|
165
|
-
expect(run).toEqual({ rebuilt: [], failed: [], unmapped: [] });
|
|
166
|
+
expect(run).toEqual({ rebuilt: [], failed: [], unmapped: [], unresolvedManaged: [] });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// #361: eine managed-Tabelle (Marker tragen nur managed), die in DIESEM Run
|
|
170
|
+
// geleert wurde, aber keine Projektion auflöst = owning-Feature fehlt in der
|
|
171
|
+
// Komposition → laut (unresolvedManaged), aber non-fatal (gedraint, kein Throw).
|
|
172
|
+
test("managed table emptied THIS run with no resolving projection → unresolvedManaged, still drained", async () => {
|
|
173
|
+
writeRebuildMarker(markerDir, "0003_orphan_managed.sql", ["read_orphan_projection"]);
|
|
174
|
+
const queued = await queueRebuildsFromMarkers(testDb.db, {
|
|
175
|
+
migrationsDir: markerDir,
|
|
176
|
+
appliedIds: ["0003_orphan_managed"],
|
|
177
|
+
});
|
|
178
|
+
expect(queued).toEqual(["read_orphan_projection"]);
|
|
179
|
+
|
|
180
|
+
const run = await runPendingRebuilds(testDb.db, registry, { thisRunTables: queued });
|
|
181
|
+
expect(run.unresolvedManaged).toEqual(["read_orphan_projection"]);
|
|
182
|
+
expect(run.unmapped).toEqual([]);
|
|
183
|
+
expect(run.rebuilt).toEqual([]);
|
|
184
|
+
expect(run.failed).toEqual([]);
|
|
185
|
+
// Trotz laut: gedraint → kein sticky-stuck Re-Apply.
|
|
186
|
+
expect(await listPendingRebuilds(testDb.db)).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// #361: dieselbe unmapped-Tabelle, aber NICHT in thisRunTables (pre-existing
|
|
190
|
+
// pending aus einem früheren Run / evtl. altem unmanaged-Marker) → benign,
|
|
191
|
+
// still gedraint, NICHT als unresolvedManaged geflaggt (kein False-Positive).
|
|
192
|
+
test("pre-existing pending table not in thisRunTables → benign unmapped, not flagged", async () => {
|
|
193
|
+
writeRebuildMarker(markerDir, "0004_legacy.sql", ["read_legacy_table"]);
|
|
194
|
+
await queueRebuildsFromMarkers(testDb.db, {
|
|
195
|
+
migrationsDir: markerDir,
|
|
196
|
+
appliedIds: ["0004_legacy"],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Re-Run ohne frisch gequeuete Tabellen (thisRunTables leer) — die Queue
|
|
200
|
+
// trägt nur den Altbestand.
|
|
201
|
+
const run = await runPendingRebuilds(testDb.db, registry, { thisRunTables: [] });
|
|
202
|
+
expect(run.unmapped).toEqual(["read_legacy_table"]);
|
|
203
|
+
expect(run.unresolvedManaged).toEqual([]);
|
|
204
|
+
expect(await listPendingRebuilds(testDb.db)).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// #361: ein lauter unresolved-managed-Eintrag darf den auflösbaren Rebuild
|
|
208
|
+
// im selben Run nicht blockieren.
|
|
209
|
+
test("mixed run: resolvable projection rebuilt while an unresolved managed table is flagged", async () => {
|
|
210
|
+
await executor.create({ groupId: GROUP, name: "a" }, admin, tdb);
|
|
211
|
+
writeRebuildMarker(markerDir, "0005_mixed.sql", [
|
|
212
|
+
"read_pending_counts",
|
|
213
|
+
"read_orphan_projection",
|
|
214
|
+
]);
|
|
215
|
+
const queued = await queueRebuildsFromMarkers(testDb.db, {
|
|
216
|
+
migrationsDir: markerDir,
|
|
217
|
+
appliedIds: ["0005_mixed"],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const run = await runPendingRebuilds(testDb.db, registry, { thisRunTables: queued });
|
|
221
|
+
expect(run.rebuilt).toEqual([
|
|
222
|
+
{ projection: "pendingtest:projection:pending-counts", eventsProcessed: 1 },
|
|
223
|
+
]);
|
|
224
|
+
expect(run.unresolvedManaged).toEqual(["read_orphan_projection"]);
|
|
225
|
+
expect(run.failed).toEqual([]);
|
|
226
|
+
expect(await listPendingRebuilds(testDb.db)).toEqual([]);
|
|
227
|
+
expect(await getCount()).toBe(1);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// #362: der framework-Helper. Ohne jobs-Feature (kein jobRunner) rebuildet er
|
|
232
|
+
// synchron inline — das heutige Verhalten, garantiert framework-pur. Der
|
|
233
|
+
// dispatch-Pfad (mit jobs) lebt in jobs/__tests__/projection-rebuild-job.*.
|
|
234
|
+
describe("enqueueProjectionRebuild — inline fallback (no jobs feature)", () => {
|
|
235
|
+
test("without a jobRunner, rebuilds the projection synchronously", async () => {
|
|
236
|
+
await executor.create({ groupId: GROUP, name: "a" }, admin, tdb);
|
|
237
|
+
await executor.create({ groupId: GROUP, name: "b" }, admin, tdb);
|
|
238
|
+
|
|
239
|
+
const outcome = await enqueueProjectionRebuild("pendingtest:projection:pending-counts", {
|
|
240
|
+
db: testDb.db,
|
|
241
|
+
registry,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(outcome.mode).toBe("inline");
|
|
245
|
+
if (outcome.mode === "inline") {
|
|
246
|
+
expect(outcome.result.eventsProcessed).toBe(2);
|
|
247
|
+
}
|
|
248
|
+
expect(await getCount()).toBe(2);
|
|
166
249
|
});
|
|
167
250
|
});
|
package/src/migrations/index.ts
CHANGED
|
@@ -10,10 +10,15 @@ export {
|
|
|
10
10
|
// Persistente Pending-Rebuild-Queue (survives Rebuild-Failures + Crashes).
|
|
11
11
|
export {
|
|
12
12
|
createPendingRebuildsTable,
|
|
13
|
+
type EnqueueProjectionRebuildDeps,
|
|
14
|
+
type EnqueueProjectionRebuildResult,
|
|
15
|
+
enqueueProjectionRebuild,
|
|
13
16
|
listPendingRebuilds,
|
|
14
17
|
type PendingRebuildRun,
|
|
18
|
+
PROJECTION_REBUILD_JOB,
|
|
15
19
|
pendingRebuildsTable,
|
|
16
20
|
queueRebuildsFromMarkers,
|
|
21
|
+
type RunPendingRebuildsOptions,
|
|
17
22
|
runPendingRebuilds,
|
|
18
23
|
} from "./pending-rebuilds";
|
|
19
24
|
// tableName → projection-name, für den app-seitigen Projection-Rebuild.
|
|
@@ -17,7 +17,9 @@ import { deleteMany, selectMany, upsertOnConflict } from "../db/query";
|
|
|
17
17
|
import { readRebuildMarker } from "../db/rebuild-marker";
|
|
18
18
|
import { tableExists } from "../db/schema-inspection";
|
|
19
19
|
import type { Registry } from "../engine/types";
|
|
20
|
-
import {
|
|
20
|
+
import type { JobRunner } from "../jobs";
|
|
21
|
+
import { createFallbackLogger } from "../logging/utils";
|
|
22
|
+
import { type RebuildResult, rebuildProjection } from "../pipeline";
|
|
21
23
|
import { unsafePushTables } from "../stack";
|
|
22
24
|
import { buildProjectionTableIndex } from "./projection-table-index";
|
|
23
25
|
|
|
@@ -78,37 +80,75 @@ export type PendingRebuildRun = {
|
|
|
78
80
|
readonly rebuilt: readonly { readonly projection: string; readonly eventsProcessed: number }[];
|
|
79
81
|
/** Fehlgeschlagene Projektionen — ihre Tabellen BLEIBEN pending. */
|
|
80
82
|
readonly failed: readonly { readonly projection: string; readonly error: string }[];
|
|
81
|
-
/** Pending-Tabellen ohne registrierte Projektion
|
|
83
|
+
/** Pending-Tabellen ohne registrierte Projektion, die NICHT in diesem Run
|
|
84
|
+
* frisch via Marker geleert wurden (pre-existing / Legacy-unmanaged-Marker)
|
|
85
|
+
* — geräumt, still (nicht von echten Legacy-Tabellen unterscheidbar). */
|
|
82
86
|
readonly unmapped: readonly string[];
|
|
87
|
+
/** In DIESEM Run via Marker geleerte managed-Tabellen ohne auflösbare
|
|
88
|
+
* Projektion = das owning-Feature fehlt in der Komposition. Geräumt (kein
|
|
89
|
+
* Stuck-Loop), aber LAUT geloggt — die Projektion ist jetzt leer. */
|
|
90
|
+
readonly unresolvedManaged: readonly string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type RunPendingRebuildsOptions = {
|
|
94
|
+
/** Tabellen, die in DIESEM apply-Run frisch via Marker gequeued wurden
|
|
95
|
+
* (Rückgabe von `queueRebuildsFromMarkers`). Marker tragen nur managed
|
|
96
|
+
* Tabellen — eine davon ohne auflösbare Projektion ist ein echter Defekt
|
|
97
|
+
* (fehlendes Feature) und wird laut gemeldet statt still geleert. Fehlt die
|
|
98
|
+
* Option, wird jede unmapped Tabelle als pre-existing/benign behandelt
|
|
99
|
+
* (Verhalten vor #361). */
|
|
100
|
+
readonly thisRunTables?: readonly string[];
|
|
83
101
|
};
|
|
84
102
|
|
|
85
103
|
/** Arbeitet die persistierte Queue ab: mappt Tabellen auf Projektionen,
|
|
86
104
|
* rebuildet jede betroffene Projektion und räumt ihre Tabellen erst nach
|
|
87
105
|
* ERFOLG aus der Queue. Fehlgeschlagene bleiben pending — der nächste
|
|
88
|
-
* apply (oder ein direkter Re-Call) holt sie nach.
|
|
106
|
+
* apply (oder ein direkter Re-Call) holt sie nach. Unmapped-Tabellen werden
|
|
107
|
+
* geräumt (kein Stuck-Loop); die in diesem Run frisch geleerten managed-
|
|
108
|
+
* Tabellen ohne Projektion zusätzlich laut gemeldet (`unresolvedManaged`). */
|
|
89
109
|
export async function runPendingRebuilds(
|
|
90
110
|
db: DbConnection,
|
|
91
111
|
registry: Registry,
|
|
112
|
+
options: RunPendingRebuildsOptions = {},
|
|
92
113
|
): Promise<PendingRebuildRun> {
|
|
93
114
|
const pending = await listPendingRebuilds(db);
|
|
94
|
-
if (pending.length === 0)
|
|
115
|
+
if (pending.length === 0) {
|
|
116
|
+
return { rebuilt: [], failed: [], unmapped: [], unresolvedManaged: [] };
|
|
117
|
+
}
|
|
95
118
|
|
|
96
119
|
const tableToProjection = buildProjectionTableIndex(registry);
|
|
120
|
+
const thisRun = new Set(options.thisRunTables ?? []);
|
|
97
121
|
const byProjection = new Map<string, string[]>();
|
|
98
122
|
const unmapped: string[] = [];
|
|
123
|
+
const unresolvedManaged: string[] = [];
|
|
99
124
|
for (const tableName of pending) {
|
|
100
125
|
const projection = tableToProjection.get(tableName);
|
|
101
126
|
if (projection === undefined) {
|
|
102
|
-
|
|
127
|
+
// Marker tragen nur managed Tabellen (rebuild-marker.ts). Eine in DIESEM
|
|
128
|
+
// Run frisch geleerte Tabelle ohne auflösbare Projektion ist daher ein
|
|
129
|
+
// echter Defekt (owning-Feature fehlt in der Komposition) → laut. Pre-
|
|
130
|
+
// existing pending Tabellen sind nicht von alten unmanaged-Markern
|
|
131
|
+
// unterscheidbar → still drainen wie bisher (kein Hard-Throw, siehe #361).
|
|
132
|
+
if (thisRun.has(tableName)) unresolvedManaged.push(tableName);
|
|
133
|
+
else unmapped.push(tableName);
|
|
103
134
|
continue;
|
|
104
135
|
}
|
|
105
136
|
byProjection.set(projection, [...(byProjection.get(projection) ?? []), tableName]);
|
|
106
137
|
}
|
|
107
138
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
// Beide Klassen räumen: die Queue darf nicht ewig wachsen, und ein Re-Apply
|
|
140
|
+
// darf nicht sticky-stuck werfen. Die Lautstärke liegt im Log + Return-Feld,
|
|
141
|
+
// nicht im Liegenlassen.
|
|
142
|
+
const drained = [...unmapped, ...unresolvedManaged];
|
|
143
|
+
if (drained.length > 0) {
|
|
144
|
+
await clearPendingRebuilds(db, drained);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (unresolvedManaged.length > 0) {
|
|
148
|
+
createFallbackLogger("migrations:pending-rebuilds").error(
|
|
149
|
+
`${unresolvedManaged.length} managed projection table(s) emptied by a migration in this run have no registered projection — the owning feature is likely missing from the composition. They are now EMPTY and were NOT rebuilt: ${unresolvedManaged.join(", ")}. Restore the owning feature or rebuild the projection manually.`,
|
|
150
|
+
{ tables: unresolvedManaged },
|
|
151
|
+
);
|
|
112
152
|
}
|
|
113
153
|
|
|
114
154
|
const rebuilt: { projection: string; eventsProcessed: number }[] = [];
|
|
@@ -122,5 +162,39 @@ export async function runPendingRebuilds(
|
|
|
122
162
|
failed.push({ projection, error: e instanceof Error ? e.message : String(e) });
|
|
123
163
|
}
|
|
124
164
|
}
|
|
125
|
-
return { rebuilt, failed, unmapped };
|
|
165
|
+
return { rebuilt, failed, unmapped, unresolvedManaged };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Qualified name of the framework-provided projection-rebuild job. Registered
|
|
169
|
+
// by the `jobs` bundled-feature (createJobsFeature) → available whenever jobs
|
|
170
|
+
// is composed. A jobs-less boot has no jobRunner and rebuilds inline instead.
|
|
171
|
+
export const PROJECTION_REBUILD_JOB = "jobs:job:projection-rebuild";
|
|
172
|
+
|
|
173
|
+
export type EnqueueProjectionRebuildResult =
|
|
174
|
+
| { readonly mode: "dispatched"; readonly bullJobId: string }
|
|
175
|
+
| { readonly mode: "inline"; readonly result: RebuildResult };
|
|
176
|
+
|
|
177
|
+
export type EnqueueProjectionRebuildDeps = {
|
|
178
|
+
readonly db: DbConnection;
|
|
179
|
+
readonly registry: Registry;
|
|
180
|
+
// Present + projection-rebuild job registered (jobs composed) → tracked,
|
|
181
|
+
// retryable job (read_job_runs + read_job_run_logs). Absent → inline rebuild.
|
|
182
|
+
readonly jobRunner?: JobRunner;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Triggert einen Single-Projection-Rebuild. Mit `jobs`-Feature (jobRunner +
|
|
186
|
+
* registriertem Job) als getrackter, retrybarer Job; ohne jobs synchron inline
|
|
187
|
+
* (heutiges Verhalten). Capability-Detektion über `registry.getJob`, NICHT
|
|
188
|
+
* `hasFeature` — deterministisch und ohne Toggle-Runtime-Abhängigkeit. */
|
|
189
|
+
export async function enqueueProjectionRebuild(
|
|
190
|
+
projection: string,
|
|
191
|
+
deps: EnqueueProjectionRebuildDeps,
|
|
192
|
+
): Promise<EnqueueProjectionRebuildResult> {
|
|
193
|
+
const { db, registry, jobRunner } = deps;
|
|
194
|
+
if (jobRunner !== undefined && registry.getJob(PROJECTION_REBUILD_JOB) !== undefined) {
|
|
195
|
+
const bullJobId = await jobRunner.dispatch(PROJECTION_REBUILD_JOB, { projection });
|
|
196
|
+
return { mode: "dispatched", bullJobId };
|
|
197
|
+
}
|
|
198
|
+
const result = await rebuildProjection(projection, { db, registry });
|
|
199
|
+
return { mode: "inline", result };
|
|
126
200
|
}
|
|
@@ -278,7 +278,11 @@ export function createDispatcher(
|
|
|
278
278
|
// Mirror notify: only built when the config feature wired its factory.
|
|
279
279
|
const config =
|
|
280
280
|
context._configAccessorFactory && db
|
|
281
|
-
? context._configAccessorFactory({
|
|
281
|
+
? context._configAccessorFactory({
|
|
282
|
+
user: { id: user.id, tenantId: user.tenantId },
|
|
283
|
+
db,
|
|
284
|
+
secrets: context.secrets,
|
|
285
|
+
})
|
|
282
286
|
: undefined;
|
|
283
287
|
|
|
284
288
|
// Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
|
|
@@ -298,9 +302,9 @@ export function createDispatcher(
|
|
|
298
302
|
// (e.g. system-privileged lookups that bypass field-access read filters).
|
|
299
303
|
const bridgeSink = afterCommitHooks ?? [];
|
|
300
304
|
const bridge = {
|
|
301
|
-
query: (targetType: string, payload: unknown) => executeQuery(targetType, payload, user, tx),
|
|
305
|
+
query: (targetType: string, payload: unknown) => executeQuery(targetType, payload, user, tx), // @wrapper-known semantic-alias
|
|
302
306
|
queryAs: (asUser: SessionUser, targetType: string, payload: unknown) =>
|
|
303
|
-
executeQuery(targetType, payload, asUser, tx),
|
|
307
|
+
executeQuery(targetType, payload, asUser, tx), // @wrapper-known semantic-alias
|
|
304
308
|
write: async (targetType: string, payload: unknown) => {
|
|
305
309
|
const res = await executeWrite(targetType, payload, user, tx, bridgeSink);
|
|
306
310
|
return res;
|
|
@@ -476,7 +480,7 @@ export function createDispatcher(
|
|
|
476
480
|
user.tenantId,
|
|
477
481
|
reducer,
|
|
478
482
|
initial,
|
|
479
|
-
{ upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx) },
|
|
483
|
+
{ upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx) }, // @wrapper-known semantic-alias
|
|
480
484
|
);
|
|
481
485
|
},
|
|
482
486
|
queryProjection: async <T = Record<string, unknown>>(
|
|
@@ -521,7 +525,7 @@ export function createDispatcher(
|
|
|
521
525
|
// handler surface just forwards the call so both entry points (login
|
|
522
526
|
// handler via ctx.resolveAuthClaims, switch-tenant route via
|
|
523
527
|
// dispatcher.resolveAuthClaims) cannot drift.
|
|
524
|
-
resolveAuthClaims: (claimsUser: SessionUser) => resolveAuthClaimsFn(claimsUser),
|
|
528
|
+
resolveAuthClaims: (claimsUser: SessionUser) => resolveAuthClaimsFn(claimsUser), // @wrapper-known semantic-alias
|
|
525
529
|
|
|
526
530
|
// Feature-effective check for in-handler opt-in logic. Scope:
|
|
527
531
|
// **current user's tenant** — for cross-tenant lookups (rare,
|
|
@@ -1382,12 +1386,16 @@ export function createDispatcher(
|
|
|
1382
1386
|
}
|
|
1383
1387
|
const db = createTenantDb(dbSource, user.tenantId, "tenant", context.tracer, context.meter);
|
|
1384
1388
|
const configAccessor = context._configAccessorFactory
|
|
1385
|
-
? context._configAccessorFactory({
|
|
1389
|
+
? context._configAccessorFactory({
|
|
1390
|
+
user: { id: user.id, tenantId: user.tenantId },
|
|
1391
|
+
db,
|
|
1392
|
+
secrets: context.secrets,
|
|
1393
|
+
})
|
|
1386
1394
|
: undefined;
|
|
1387
1395
|
return {
|
|
1388
1396
|
db,
|
|
1389
1397
|
queryAs: (asUser: SessionUser, qn: string, payload: unknown) =>
|
|
1390
|
-
executeQuery(qn, payload, asUser),
|
|
1398
|
+
executeQuery(qn, payload, asUser), // @wrapper-known semantic-alias
|
|
1391
1399
|
...(configAccessor && { config: configAccessor }),
|
|
1392
1400
|
};
|
|
1393
1401
|
}
|
|
@@ -286,8 +286,8 @@ export function createLifecycleHooks(
|
|
|
286
286
|
payload: result,
|
|
287
287
|
context,
|
|
288
288
|
entityName: result.entityName,
|
|
289
|
-
getHandlerHooks: (n) => registry.getPostSaveHooks(n, phase, eff),
|
|
290
|
-
getEntityHooks: (n) => registry.getEntityPostSaveHooks(n, phase, eff),
|
|
289
|
+
getHandlerHooks: (n) => registry.getPostSaveHooks(n, phase, eff), // @wrapper-known semantic-alias
|
|
290
|
+
getEntityHooks: (n) => registry.getEntityPostSaveHooks(n, phase, eff), // @wrapper-known semantic-alias
|
|
291
291
|
systemHookDefs: systemHooks.postSave,
|
|
292
292
|
phaseLabel: `postSave:${phase}`,
|
|
293
293
|
hookPhase: phase,
|
|
@@ -328,8 +328,8 @@ export function createLifecycleHooks(
|
|
|
328
328
|
payload,
|
|
329
329
|
context,
|
|
330
330
|
entityName: payload.entityName,
|
|
331
|
-
getHandlerHooks: (n) => registry.getPostDeleteHooks(n, phase, eff),
|
|
332
|
-
getEntityHooks: (n) => registry.getEntityPostDeleteHooks(n, phase, eff),
|
|
331
|
+
getHandlerHooks: (n) => registry.getPostDeleteHooks(n, phase, eff), // @wrapper-known semantic-alias
|
|
332
|
+
getEntityHooks: (n) => registry.getEntityPostDeleteHooks(n, phase, eff), // @wrapper-known semantic-alias
|
|
333
333
|
systemHookDefs: systemHooks.postDelete,
|
|
334
334
|
phaseLabel: `postDelete:${phase}`,
|
|
335
335
|
hookPhase: phase,
|
package/src/time/tz-context.ts
CHANGED
|
@@ -74,9 +74,9 @@ export function createTzContext(options: TzContextOptions = {}): TzContext {
|
|
|
74
74
|
return {
|
|
75
75
|
tenant,
|
|
76
76
|
user,
|
|
77
|
-
now: () => T.Now.instant(),
|
|
78
|
-
nowIn: (tz: string) => T.Now.zonedDateTimeISO(tz),
|
|
79
|
-
today: (tz: string) => T.Now.plainDateISO(tz),
|
|
77
|
+
now: () => T.Now.instant(), // @wrapper-known semantic-alias
|
|
78
|
+
nowIn: (tz: string) => T.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
|
|
79
|
+
today: (tz: string) => T.Now.plainDateISO(tz), // @wrapper-known semantic-alias
|
|
80
80
|
todayRange: (tz: string) => {
|
|
81
81
|
const today = T.Now.plainDateISO(tz);
|
|
82
82
|
const startZdt = today.toZonedDateTime({ timeZone: tz });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Locale-independent string ordering for sorts that feed byte-exact serialized
|
|
2
|
+
// artifacts (manifest JSON, snapshot.json, generated migration SQL). Unlike
|
|
3
|
+
// String.localeCompare — whose order depends on the runner's ICU locale and so
|
|
4
|
+
// can drift between a macOS dev box and Linux CI (#330) — this compares by
|
|
5
|
+
// UTF-16 code unit, which is stable across machines and, for the BMP identifier
|
|
6
|
+
// strings we sort here (table / column / feature / qualified names), equals
|
|
7
|
+
// codepoint order.
|
|
8
|
+
export function compareByCodepoint(a: string, b: string): number {
|
|
9
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
10
|
+
}
|
package/src/utils/ids.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { v7 } from "uuid";
|
|
|
11
11
|
// Do NOT use this for security tokens (CSRF, session, API keys). The
|
|
12
12
|
// timestamp prefix leaks creation time and shrinks unpredictable
|
|
13
13
|
// entropy from 122 to 74 bits — use `generateToken` from api/tokens.ts.
|
|
14
|
+
// @wrapper-known semantic-alias
|
|
14
15
|
export function generateId(): string {
|
|
15
16
|
return v7();
|
|
16
17
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { assertUnreachable } from "./assert";
|
|
2
2
|
export { toSnakeCase } from "./case";
|
|
3
|
+
export { compareByCodepoint } from "./compare";
|
|
3
4
|
export { readPositiveIntEnv } from "./env-parse";
|
|
4
5
|
export { generateId } from "./ids";
|
|
5
6
|
export { isPlainObject } from "./is-plain-object";
|