@cosmicdrift/kumiko-framework 0.384.0 → 0.386.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/agents/examples/i18n.md +36 -1
- package/agents/features.md +4 -0
- package/dist/api/auth-rate-limiters.d.ts +8 -0
- package/dist/api/auth-rate-limiters.js +105 -0
- package/dist/api/auth-routes-context.d.ts +34 -0
- package/dist/api/auth-routes-context.js +192 -0
- package/dist/api/auth-routes-invite.d.ts +2 -0
- package/dist/api/auth-routes-invite.js +171 -0
- package/dist/api/auth-routes-login.d.ts +2 -0
- package/dist/api/auth-routes-login.js +103 -0
- package/dist/api/auth-routes-mfa.d.ts +4 -0
- package/dist/api/auth-routes-mfa.js +211 -0
- package/dist/api/auth-routes-session.d.ts +2 -0
- package/dist/api/auth-routes-session.js +144 -0
- package/dist/api/auth-routes-shared.d.ts +33 -0
- package/dist/api/auth-routes-shared.js +81 -0
- package/dist/api/auth-routes-signup.d.ts +2 -0
- package/dist/api/auth-routes-signup.js +75 -0
- package/dist/api/auth-routes-token-flows.d.ts +4 -0
- package/dist/api/auth-routes-token-flows.js +91 -0
- package/dist/api/auth-routes-types.d.ts +76 -0
- package/dist/api/auth-routes-types.js +1 -0
- package/dist/api/auth-routes.d.ts +5 -85
- package/dist/api/auth-routes.js +20 -1095
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- package/dist/config/index.d.ts +1 -1
- package/dist/crypto/pii-ciphertext-format.js +1 -1
- package/dist/db/blind-index-cleanup.js +1 -1
- package/dist/db/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +9 -0
- package/dist/db/distinct-on.js +28 -0
- package/dist/db/event-store-executor-write.js +2 -2
- package/dist/db/migrate-runner.js +4 -4
- package/dist/db/pg-error.js +0 -12
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/postgres-array-literal.js +2 -2
- package/dist/db/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- package/dist/db/query.js +19 -19
- package/dist/db/tenant-db.js +1 -1
- package/dist/engine/boot-validator/i18n-keys.d.ts +1 -0
- package/dist/engine/boot-validator/i18n-keys.js +8 -1
- package/dist/engine/boot-validator/index.d.ts +4 -0
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +67 -0
- package/dist/engine/boot-validator/screens-actions.js +488 -0
- package/dist/engine/boot-validator/screens-dashboard.d.ts +15 -0
- package/dist/engine/boot-validator/screens-dashboard.js +467 -0
- package/dist/engine/boot-validator/screens-forms.d.ts +24 -0
- package/dist/engine/boot-validator/screens-forms.js +516 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +47 -0
- package/dist/engine/boot-validator/screens-lists.js +319 -0
- package/dist/engine/boot-validator/screens.d.ts +4 -21
- package/dist/engine/boot-validator/screens.js +26 -1799
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
- package/dist/engine/steps/read-find-one.js +1 -1
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/file-routes.js +1 -1
- package/dist/i18n/required-surface-keys.d.ts +16 -0
- package/dist/i18n/required-surface-keys.js +62 -0
- package/dist/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +44 -0
- package/dist/jobs/job-runner-helpers.js +265 -0
- package/dist/jobs/job-runner-schedulers.d.ts +23 -0
- package/dist/jobs/job-runner-schedulers.js +106 -0
- package/dist/jobs/job-runner-types.d.ts +65 -0
- package/dist/jobs/job-runner-types.js +2 -0
- package/dist/jobs/job-runner.d.ts +4 -75
- package/dist/jobs/job-runner.js +10 -386
- package/dist/pipeline/dispatch-shared.js +29 -4
- package/dist/pipeline/event-dispatcher.js +2 -3
- package/dist/schema-cli.js +5 -0
- package/package.json +3 -3
- package/src/changes.json +19 -0
|
@@ -21,6 +21,7 @@ export type RequestContextData = {
|
|
|
21
21
|
readonly risk: AgentRisk;
|
|
22
22
|
};
|
|
23
23
|
readonly chargedRateLimitBuckets?: Set<string>;
|
|
24
|
+
readonly agentToolCall?: true;
|
|
24
25
|
};
|
|
25
26
|
export declare const requestContext: {
|
|
26
27
|
run<T>(data: RequestContextData, fn: () => T): T;
|
|
@@ -41,4 +42,5 @@ export declare function runWithOrigin<T>(origin: {
|
|
|
41
42
|
};
|
|
42
43
|
}, fn: () => T): T;
|
|
43
44
|
export declare function runAsDirectCallEntry<T>(fn: () => T): T;
|
|
45
|
+
export declare function isAgentToolCallScope(): boolean;
|
|
44
46
|
export declare function runWithWriteOrigin<T>(origin: WriteOrigin, fn: () => T): T;
|
|
@@ -48,15 +48,24 @@ export function runWithOrigin(origin, fn) {
|
|
|
48
48
|
: new Set(),
|
|
49
49
|
}, fn);
|
|
50
50
|
}
|
|
51
|
-
// Clears entryHandler so the next dispatch inside fn becomes the new entry
|
|
52
|
-
//
|
|
53
|
-
// turn/approve handler
|
|
51
|
+
// Clears entryHandler so the next dispatch inside fn becomes the new entry, letting
|
|
52
|
+
// the irreversible-operation gate see the invoked tool handler instead of the
|
|
53
|
+
// surrounding turn/approve handler. Reserved for dispatchToolCall, so it also marks
|
|
54
|
+
// the scope as an agent tool call; it always mints a scope, because an agent loop
|
|
55
|
+
// running outside any request must still carry that marker.
|
|
54
56
|
export function runAsDirectCallEntry(fn) {
|
|
55
57
|
const current = requestContext.get();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const requestId = current?.requestId ?? requestContext.generateId();
|
|
59
|
+
const { entryHandler: _entryHandler, ...rest } = current ?? {};
|
|
60
|
+
return requestContext.run({
|
|
61
|
+
...rest,
|
|
62
|
+
requestId,
|
|
63
|
+
correlationId: current?.correlationId ?? requestId,
|
|
64
|
+
agentToolCall: true,
|
|
65
|
+
}, fn);
|
|
66
|
+
}
|
|
67
|
+
export function isAgentToolCallScope() {
|
|
68
|
+
return requestContext.get()?.agentToolCall === true;
|
|
60
69
|
}
|
|
61
70
|
// An ungated origin never mints a scope: seeds and boot writes keep their missing context.
|
|
62
71
|
export function runWithWriteOrigin(origin, fn) {
|
package/dist/config/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type AppClientConfig = {
|
|
|
21
21
|
readonly entries: readonly string[];
|
|
22
22
|
};
|
|
23
23
|
/** The boot-validator options an app owns; `env` and the cipher are injected by the runners. */
|
|
24
|
-
export type AppValidateBootOptions = Pick<ValidateBootOptions, "navAllowlist" | "navAllowlistExempt" | "warnOnUniqueAccessRoles">;
|
|
24
|
+
export type AppValidateBootOptions = Pick<ValidateBootOptions, "navAllowlist" | "navAllowlistExempt" | "requiredLocales" | "warnOnUniqueAccessRoles">;
|
|
25
25
|
export type AppConfig = {
|
|
26
26
|
/** Features mounted in every environment: dev, tests, prod. */
|
|
27
27
|
readonly features: readonly FeatureDefinition[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Leaf module (no engine imports) so
|
|
1
|
+
// Leaf module (no engine imports) so the query helpers and the executors can sniff PII
|
|
2
2
|
// ciphertext without pulling the KMS/pipeline graph into an import cycle.
|
|
3
3
|
export const PII_CIPHERTEXT_PREFIX_V1 = "kumiko-pii:v1:";
|
|
4
4
|
export const PII_CIPHERTEXT_PREFIX = "kumiko-pii:v2:";
|
|
@@ -77,7 +77,7 @@ async function existingTableNames(db, tableNames) {
|
|
|
77
77
|
export async function subjectRowExistsInTenant(db, features, subjectId, tenantId) {
|
|
78
78
|
// Prefetch which self-PII projection tables actually exist — probing a
|
|
79
79
|
// missing relation used to throw (and get swallowed), which both hid
|
|
80
|
-
// real schema bugs and risked poisoning the
|
|
80
|
+
// real schema bugs and risked poisoning the driver connection for the
|
|
81
81
|
// rest of the forget TX (fw#2348 / framework#356).
|
|
82
82
|
const candidateTables = [];
|
|
83
83
|
for (const feature of features.values()) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CrossTenantReads } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
2
|
+
import type { EscapeHatchReporter } from "../engine/types/index.ts";
|
|
3
|
+
import { type CreateTenantDbOptions, type TenantDb } from "./tenant-db.ts";
|
|
4
|
+
export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): CrossTenantReads;
|
|
5
|
+
export declare function createHandlerCrossTenantReads(options: Omit<CreateTenantDbOptions, "mode"> & {
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
readonly report: EscapeHatchReporter;
|
|
8
|
+
}): CrossTenantReads;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTenantDb } from "./tenant-db.js";
|
|
2
|
+
export function createCrossTenantReads(systemModeDb, reason, report) {
|
|
3
|
+
return {
|
|
4
|
+
selectMany(table, where, options) {
|
|
5
|
+
report("cross-tenant-read", reason);
|
|
6
|
+
return systemModeDb.selectMany(table, where, options);
|
|
7
|
+
},
|
|
8
|
+
fetchOne(table, where) {
|
|
9
|
+
report("cross-tenant-read", reason);
|
|
10
|
+
return systemModeDb.fetchOne(table, where);
|
|
11
|
+
},
|
|
12
|
+
count(table, where) {
|
|
13
|
+
report("cross-tenant-read", reason);
|
|
14
|
+
return systemModeDb.count(table, where);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Own system-mode TenantDb without unsafeRaw/globalWrites: the handler gets the read surface only.
|
|
19
|
+
export function createHandlerCrossTenantReads(options) {
|
|
20
|
+
const { reason, report, ...dbOptions } = options;
|
|
21
|
+
const systemModeDb = createTenantDb({ ...dbOptions, mode: "system" });
|
|
22
|
+
return createCrossTenantReads(systemModeDb, reason, report);
|
|
23
|
+
}
|
package/dist/db/dialect.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Native dialect — replaces drizzle-orm/pg-core re-exports.
|
|
2
2
|
//
|
|
3
3
|
// Produces table objects that simultaneously expose:
|
|
4
|
-
// 1. EntityTableMeta shape (source/columns/indexes) for
|
|
4
|
+
// 1. EntityTableMeta shape (source/columns/indexes) for the query helpers'
|
|
5
5
|
// extractTableInfo + the migrate-runner's renderTableDdl
|
|
6
6
|
// 2. drizzle-compatible Symbol metadata so callers that introspect
|
|
7
7
|
// Symbol.for("kumiko:schema:Name") / Symbol.for("kumiko:schema:Columns") keep
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OrderByClause } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
2
|
+
import type { TableInfo } from "./query.ts";
|
|
3
|
+
type DistinctOnClause = {
|
|
4
|
+
/** Physical column names, unquoted. */
|
|
5
|
+
readonly columns: readonly string[];
|
|
6
|
+
readonly orderBy: readonly OrderByClause[];
|
|
7
|
+
};
|
|
8
|
+
export declare function buildDistinctOnClause(info: TableInfo, distinctOn: readonly string[] | undefined, orderBy: OrderByClause | readonly OrderByClause[] | undefined): DistinctOnClause | undefined;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Postgres rejects DISTINCT ON unless ORDER BY starts with the same
|
|
2
|
+
// expressions, so a mismatch is caught here with a readable message instead of
|
|
3
|
+
// surfacing as a driver error.
|
|
4
|
+
export function buildDistinctOnClause(info, distinctOn, orderBy) {
|
|
5
|
+
if (distinctOn === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (distinctOn.length === 0) {
|
|
8
|
+
throw new Error(`selectMany: distinctOn must not be empty for "${info.name}"`);
|
|
9
|
+
}
|
|
10
|
+
for (const field of distinctOn) {
|
|
11
|
+
if (!info.hasColumn(field)) {
|
|
12
|
+
throw new Error(`selectMany: distinctOn entry "${field}" is not a column of "${info.name}"`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const columns = distinctOn.map((field) => info.columnOf(field));
|
|
16
|
+
if (orderBy === undefined) {
|
|
17
|
+
return { columns, orderBy: distinctOn.map((col) => ({ col })) };
|
|
18
|
+
}
|
|
19
|
+
const clauses = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
20
|
+
const startsWithDistinctOn = columns.every((column, i) => {
|
|
21
|
+
const clause = clauses[i];
|
|
22
|
+
return (clause !== undefined && info.hasColumn(clause.col) && info.columnOf(clause.col) === column);
|
|
23
|
+
});
|
|
24
|
+
if (!startsWithDistinctOn) {
|
|
25
|
+
throw new Error(`selectMany: orderBy must start with the distinctOn fields [${distinctOn.join(", ")}] in the same order for "${info.name}"`);
|
|
26
|
+
}
|
|
27
|
+
return { columns, orderBy: clauses };
|
|
28
|
+
}
|
|
@@ -290,7 +290,7 @@ export function createWriteVerbs(ctx) {
|
|
|
290
290
|
// selben catch (siehe line 493+).
|
|
291
291
|
let event;
|
|
292
292
|
try {
|
|
293
|
-
// Savepoint-scoped: postgres.js
|
|
293
|
+
// Savepoint-scoped: postgres.js poisons the WHOLE surrounding
|
|
294
294
|
// begin() once any statement inside it errors, even if the JS error
|
|
295
295
|
// is caught (kumiko-framework#1778) — a losing concurrent create's
|
|
296
296
|
// unique-violation would otherwise abort the caller's outer
|
|
@@ -336,7 +336,7 @@ export function createWriteVerbs(ctx) {
|
|
|
336
336
|
// UniqueViolationError 409 damit Designer/Frontend einen sauberen
|
|
337
337
|
// "duplicate" zeigen können statt cryptic "internal server error".
|
|
338
338
|
// Savepoint like the append above: an unclassified DB error out of the
|
|
339
|
-
// projection INSERT would otherwise poison the enclosing tx (
|
|
339
|
+
// projection INSERT would otherwise poison the enclosing tx (postgres-js
|
|
340
340
|
// rejects the whole begin() once any statement in it failed), so a later
|
|
341
341
|
// write sharing that tx — a sibling write after a failed nested one —
|
|
342
342
|
// throws raw as a 500 instead of failing cleanly. The savepoint rolls the
|
|
@@ -188,16 +188,16 @@ function migrationClient(db) {
|
|
|
188
188
|
if (hasBegin(client))
|
|
189
189
|
return client;
|
|
190
190
|
}
|
|
191
|
-
throw new Error("migrate-runner: db argument has no .unsafe() and .begin() (need
|
|
191
|
+
throw new Error("migrate-runner: db argument has no .unsafe() and .begin() (need a postgres-js Sql or drizzle DbConnection)");
|
|
192
192
|
}
|
|
193
|
-
// Raw-SQL via postgres-js
|
|
193
|
+
// Raw-SQL via postgres-js .unsafe().
|
|
194
194
|
async function executeRaw(db, sqlText) {
|
|
195
195
|
await migrationClient(db).unsafe(sqlText);
|
|
196
196
|
}
|
|
197
197
|
async function reserveConnection(db) {
|
|
198
198
|
const client = migrationClient(db);
|
|
199
199
|
if (typeof client.reserve !== "function") {
|
|
200
|
-
throw new Error("migrate-runner: db argument has no .reserve() (need
|
|
200
|
+
throw new Error("migrate-runner: db argument has no .reserve() (need a postgres-js Sql)");
|
|
201
201
|
}
|
|
202
202
|
return client.reserve();
|
|
203
203
|
}
|
|
@@ -235,7 +235,7 @@ export class MigrationChecksumMismatchError extends Error {
|
|
|
235
235
|
// Advisory-Lock verhindert concurrent-apply-races.
|
|
236
236
|
export async function runMigrations(db, migrations) {
|
|
237
237
|
// Session-level advisory lock and per-migration BEGIN/COMMIT must share one
|
|
238
|
-
// session. Pool calls may land on different connections (
|
|
238
|
+
// session. Pool calls may land on different connections (the pool round-robins),
|
|
239
239
|
// which would leave the lock held on a session nobody unlocks.
|
|
240
240
|
const conn = await reserveConnection(db);
|
|
241
241
|
try {
|
package/dist/db/pg-error.js
CHANGED
|
@@ -20,18 +20,6 @@ export function extractPgError(e) {
|
|
|
20
20
|
// @cast-boundary error-details — postgres-js error shape (code, constraint_name)
|
|
21
21
|
const code = layer.code;
|
|
22
22
|
const constraintName = layer.constraint_name; // @cast-boundary error-details
|
|
23
|
-
// Bun.SQL carries the SQLSTATE in `errno` (string) and the constraint name
|
|
24
|
-
// in `constraint`, not in postgres-js's `code`/`constraint_name`.
|
|
25
|
-
if (code === "ERR_POSTGRES_SERVER_ERROR") {
|
|
26
|
-
const errno = layer.errno; // @cast-boundary error-details
|
|
27
|
-
const constraint = layer.constraint; // @cast-boundary error-details
|
|
28
|
-
if (typeof errno === "string") {
|
|
29
|
-
return {
|
|
30
|
-
code: errno,
|
|
31
|
-
constraint_name: constraintName ?? (typeof constraint === "string" ? constraint : undefined),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
23
|
if (code !== undefined || constraintName !== undefined) {
|
|
36
24
|
return { code, constraint_name: constraintName };
|
|
37
25
|
}
|
|
@@ -178,7 +178,7 @@ export async function updateConsumerDeliveryOutcome(db, name, instanceId, outcom
|
|
|
178
178
|
"status" = $3,
|
|
179
179
|
"last_error" = $4,
|
|
180
180
|
"rearm_count" = CASE WHEN $5 THEN 0 ELSE "rearm_count" END,
|
|
181
|
-
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under
|
|
181
|
+
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under the driver
|
|
182
182
|
"pending_gaps" = $6::text::jsonb,
|
|
183
183
|
"last_failed_event_id" = $9,
|
|
184
184
|
"updated_at" = now()
|
|
@@ -210,7 +210,7 @@ pendingGaps) {
|
|
|
210
210
|
"last_error" = NULL,
|
|
211
211
|
"rearm_count" = 0,
|
|
212
212
|
"last_failed_event_id" = NULL,
|
|
213
|
-
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under
|
|
213
|
+
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under the driver
|
|
214
214
|
"pending_gaps" = $4::text::jsonb,
|
|
215
215
|
"updated_at" = now()
|
|
216
216
|
WHERE "name" = $2 AND "instance_id" = $3
|
|
@@ -239,7 +239,7 @@ export async function updateConsumerRebuildCursor(db, name, instanceId, lastProc
|
|
|
239
239
|
"status" = 'idle',
|
|
240
240
|
"attempts" = 0,
|
|
241
241
|
"last_error" = NULL,
|
|
242
|
-
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under
|
|
242
|
+
-- text param + cast: a JS string bound straight to ::jsonb double-encodes under the driver
|
|
243
243
|
"pending_gaps" = $2::text::jsonb,
|
|
244
244
|
"last_failed_event_id" = NULL,
|
|
245
245
|
"updated_at" = now()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Array params travel as a Postgres array-literal string (`$1::text[]`) so the
|
|
2
|
+
// binding does not depend on how the driver infers JS array types.
|
|
3
3
|
export function toPostgresArrayLiteral(values) {
|
|
4
4
|
const elements = values.map((value) => value === null ? "NULL" : `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
|
|
5
5
|
return `{${elements.join(",")}}`;
|
|
@@ -2,5 +2,8 @@ import type { AnyDb } from "../query.ts";
|
|
|
2
2
|
export { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./ddl.ts";
|
|
3
3
|
export declare function truncateTablesRestartIdentity(db: AnyDb, tableNames: readonly string[]): Promise<void>;
|
|
4
4
|
export declare function databaseExists(db: AnyDb, dbName: string): Promise<boolean>;
|
|
5
|
-
export declare function createDatabase(db: AnyDb, dbName: string): Promise<void>;
|
|
5
|
+
export declare function createDatabase(db: AnyDb, dbName: string, templateDbName?: string): Promise<void>;
|
|
6
|
+
export declare function renameDatabase(db: AnyDb, fromName: string, toName: string): Promise<void>;
|
|
7
|
+
export declare function listDatabaseNamesWithPrefix(db: AnyDb, prefix: string): Promise<readonly string[]>;
|
|
8
|
+
export declare function acquireSessionAdvisoryLock(db: AnyDb, key: string): Promise<void>;
|
|
6
9
|
export declare function dropDatabaseIfExists(db: AnyDb, dbName: string): Promise<void>;
|
|
@@ -12,8 +12,21 @@ export async function databaseExists(db, dbName) {
|
|
|
12
12
|
const rows = await asRawClient(db).unsafe(`SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS exists`, [dbName]);
|
|
13
13
|
return rows[0]?.exists === true;
|
|
14
14
|
}
|
|
15
|
-
export async function createDatabase(db, dbName) {
|
|
16
|
-
|
|
15
|
+
export async function createDatabase(db, dbName, templateDbName) {
|
|
16
|
+
const template = templateDbName === undefined ? "" : ` TEMPLATE ${quoteTableIdent(templateDbName)}`;
|
|
17
|
+
await asRawClient(db).unsafe(`CREATE DATABASE ${quoteTableIdent(dbName)}${template}`);
|
|
18
|
+
}
|
|
19
|
+
export async function renameDatabase(db, fromName, toName) {
|
|
20
|
+
await asRawClient(db).unsafe(`ALTER DATABASE ${quoteTableIdent(fromName)} RENAME TO ${quoteTableIdent(toName)}`);
|
|
21
|
+
}
|
|
22
|
+
export async function listDatabaseNamesWithPrefix(db, prefix) {
|
|
23
|
+
const rows = await asRawClient(db).unsafe(`SELECT datname FROM pg_database WHERE starts_with(datname, $1) ORDER BY datname`, [prefix]);
|
|
24
|
+
return rows.map((row) => row.datname);
|
|
25
|
+
}
|
|
26
|
+
// Session-scoped: CREATE DATABASE cannot run inside a transaction, so the
|
|
27
|
+
// xact-scoped variant cannot guard it. Released when the session closes.
|
|
28
|
+
export async function acquireSessionAdvisoryLock(db, key) {
|
|
29
|
+
await asRawClient(db).unsafe(`SELECT pg_advisory_lock(hashtext($1))`, [key]);
|
|
17
30
|
}
|
|
18
31
|
export async function dropDatabaseIfExists(db, dbName) {
|
|
19
32
|
await asRawClient(db).unsafe(`DROP DATABASE IF EXISTS ${quoteTableIdent(dbName)}`);
|
package/dist/db/query.js
CHANGED
|
@@ -24,6 +24,7 @@ import { InternalError } from "../errors/index.js";
|
|
|
24
24
|
import { createFallbackLogger } from "../logging/index.js";
|
|
25
25
|
import { parseJsonSafe } from "../utils/safe-json.js";
|
|
26
26
|
import { SQL_EXPR_BRAND } from "./dialect.js";
|
|
27
|
+
import { buildDistinctOnClause } from "./distinct-on.js";
|
|
27
28
|
import { executorManagedWriteDenied } from "./executor-managed-tables.js";
|
|
28
29
|
import { extractPgError } from "./pg-error.js";
|
|
29
30
|
import { toSnakeCase } from "./table-builder.js";
|
|
@@ -730,7 +731,6 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
730
731
|
// Each failed attempt discards one, so retries are bounded by pool size.
|
|
731
732
|
const CLOSED_CONNECTION_CODES = new Set([
|
|
732
733
|
"CONNECTION_CLOSED", // postgres-js
|
|
733
|
-
"ERR_POSTGRES_CONNECTION_CLOSED", // Bun.SQL
|
|
734
734
|
"57P01", // PG SQLSTATE: admin_shutdown / terminated backend
|
|
735
735
|
]);
|
|
736
736
|
export function isClosedConnectionError(err) {
|
|
@@ -756,14 +756,10 @@ function isDriverClosedAbort(err) {
|
|
|
756
756
|
function isHandleLike(raw) {
|
|
757
757
|
return raw !== null && (typeof raw === "object" || typeof raw === "function");
|
|
758
758
|
}
|
|
759
|
-
//
|
|
760
|
-
// handles are plain function objects, so discriminate by members only one driver
|
|
761
|
-
// has: `flush` on Bun.SQL (pool and tx), `typed` on postgres-js (pool and tx).
|
|
759
|
+
// postgres-js pool and tx handles both carry `typed`.
|
|
762
760
|
function driverOf(raw) {
|
|
763
761
|
if (!isHandleLike(raw))
|
|
764
762
|
return "unknown";
|
|
765
|
-
if (typeof raw["flush"] === "function")
|
|
766
|
-
return "bun-sql";
|
|
767
763
|
if (typeof raw["typed"] === "function")
|
|
768
764
|
return "postgres-js";
|
|
769
765
|
return "unknown";
|
|
@@ -771,13 +767,12 @@ function driverOf(raw) {
|
|
|
771
767
|
function positiveNumber(v) {
|
|
772
768
|
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
773
769
|
}
|
|
774
|
-
// postgres-js configures timeouts in seconds
|
|
775
|
-
function timeoutOptionMs(options, driver,
|
|
776
|
-
if (driver
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
return positiveNumber(options[bunName]);
|
|
770
|
+
// postgres-js configures timeouts in seconds.
|
|
771
|
+
function timeoutOptionMs(options, driver, optionName) {
|
|
772
|
+
if (driver !== "postgres-js")
|
|
773
|
+
return undefined;
|
|
774
|
+
const seconds = positiveNumber(options[optionName]);
|
|
775
|
+
return seconds === undefined ? undefined : seconds * 1000;
|
|
781
776
|
}
|
|
782
777
|
function errorProp(err, name) {
|
|
783
778
|
return isHandleLike(err) ? err[name] : undefined;
|
|
@@ -794,9 +789,9 @@ export function describeClosedConnectionFailure(raw, err, attempts) {
|
|
|
794
789
|
pooled: isPooledClient(raw),
|
|
795
790
|
attempts,
|
|
796
791
|
poolMax: positiveNumber(options["max"]),
|
|
797
|
-
idleTimeoutMs: timeoutOptionMs(options, driver, "
|
|
798
|
-
maxLifetimeMs: timeoutOptionMs(options, driver, "
|
|
799
|
-
connectionTimeoutMs: timeoutOptionMs(options, driver, "
|
|
792
|
+
idleTimeoutMs: timeoutOptionMs(options, driver, "idle_timeout"),
|
|
793
|
+
maxLifetimeMs: timeoutOptionMs(options, driver, "max_lifetime"),
|
|
794
|
+
connectionTimeoutMs: timeoutOptionMs(options, driver, "connect_timeout"),
|
|
800
795
|
errorName: errorProp(err, "name"),
|
|
801
796
|
errorCode: extractPgError(err)?.code ?? errorProp(err, "code"),
|
|
802
797
|
errorErrno: errorProp(err, "errno"),
|
|
@@ -889,15 +884,20 @@ export async function selectMany(db, table, where, options) {
|
|
|
889
884
|
return scoped.selectMany(table, where, options);
|
|
890
885
|
}
|
|
891
886
|
const info = extractTableInfo(table);
|
|
892
|
-
|
|
887
|
+
const distinctOn = buildDistinctOnClause(info, options?.distinctOn, options?.orderBy);
|
|
888
|
+
const distinctPrefix = distinctOn === undefined
|
|
889
|
+
? ""
|
|
890
|
+
: `DISTINCT ON (${distinctOn.columns.map(quoteIdent).join(", ")}) `;
|
|
891
|
+
let sqlText = `SELECT ${distinctPrefix}${selectColumnList(info, options?.fields)} FROM ${quoteIdent(info.name)}`;
|
|
893
892
|
let values = [];
|
|
894
893
|
if (where && Object.keys(where).length > 0) {
|
|
895
894
|
const w = buildWhereClause(info, where, 1);
|
|
896
895
|
sqlText += ` WHERE ${w.sqlText}`;
|
|
897
896
|
values = w.values;
|
|
898
897
|
}
|
|
899
|
-
|
|
900
|
-
|
|
898
|
+
const orderBy = distinctOn?.orderBy ?? options?.orderBy;
|
|
899
|
+
if (orderBy) {
|
|
900
|
+
const clauses = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
901
901
|
const parts = clauses.map((c) => {
|
|
902
902
|
const col = info.columnOf(c.col);
|
|
903
903
|
const dir = c.direction === "desc" ? "DESC" : "ASC";
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -33,7 +33,7 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
|
|
|
33
33
|
if (typeof value !== "function")
|
|
34
34
|
return value;
|
|
35
35
|
if (prop === "reserve") {
|
|
36
|
-
//
|
|
36
|
+
// postgres-js hands back a raw connection; re-wrap it so the gate survives.
|
|
37
37
|
return async (...args) => {
|
|
38
38
|
const reserved = await Reflect.apply(value, target, args);
|
|
39
39
|
return boundRunner(reserved, gate, projectionRegistry, executorManagedWrites);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "../types/index.ts";
|
|
2
2
|
export declare function validateI18nSurfaceKeys(features: readonly FeatureDefinition[]): void;
|
|
3
3
|
export declare function validateTranslationLocaleConsistency(features: readonly FeatureDefinition[]): void;
|
|
4
|
+
export declare function validateRequiredLocales(features: readonly FeatureDefinition[], requiredLocales: readonly string[] | undefined): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildEffectiveTranslationKeys, featureHasI18nSurface, findTranslationLocaleGaps, requiredKeysFromFeature, requiredKeysFromNav, requiredKeysFromScreen, requiredKeysFromWorkspace, screenTitleKey, } from "../../i18n/required-surface-keys.js";
|
|
1
|
+
import { buildEffectiveTranslationKeys, featureHasI18nSurface, findRequiredLocaleGaps, findTranslationLocaleGaps, requiredKeysFromFeature, requiredKeysFromNav, requiredKeysFromScreen, requiredKeysFromWorkspace, screenTitleKey, } from "../../i18n/required-surface-keys.js";
|
|
2
2
|
import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "../build-config-feature-schema.js";
|
|
3
3
|
import { EXTENSION_SELECTOR_HINT_KEY } from "../extension-selector-plugins.js";
|
|
4
4
|
import { createRegistry } from "../registry.js";
|
|
@@ -93,3 +93,10 @@ export function validateTranslationLocaleConsistency(features) {
|
|
|
93
93
|
throw new Error(`${lines.join("\n")}\nEvery locale a feature uses must be maintained for every one of its translation keys; otherwise move that locale into a locale package.`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
export function validateRequiredLocales(features, requiredLocales) {
|
|
97
|
+
const gaps = findRequiredLocaleGaps(features, requiredLocales ?? []);
|
|
98
|
+
if (gaps.length > 0) {
|
|
99
|
+
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
100
|
+
throw new Error(`${lines.join("\n")}\nThe app requires locales [${(requiredLocales ?? []).join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -32,6 +32,10 @@ export type ValidateBootOptions = {
|
|
|
32
32
|
* `masterKey` or the env keyring). Satisfies the availability check for
|
|
33
33
|
* `encrypted: true` fields without an env KEK. */
|
|
34
34
|
readonly entityFieldCipher?: EnvelopeCipher;
|
|
35
|
+
/** Locales every translation key must carry across all mounted features
|
|
36
|
+
* (locale packages count). Without it a key can ship in one language and
|
|
37
|
+
* silently fall back at runtime for users of the others. */
|
|
38
|
+
readonly requiredLocales?: readonly string[];
|
|
35
39
|
};
|
|
36
40
|
/**
|
|
37
41
|
* Subset-safe slice of validateBoot for test stacks: nav references (screen,
|
|
@@ -15,7 +15,7 @@ import { validateEntityListScreens } from "./entity-list-screens.js";
|
|
|
15
15
|
import { validateGdprStoragePersistence } from "./gdpr-storage.js";
|
|
16
16
|
import { validateGlobalTenancyEntities } from "./global-tenancy.js";
|
|
17
17
|
import { validateHandlerFieldReferences } from "./handler-field-references.js";
|
|
18
|
-
import { validateI18nSurfaceKeys, validateTranslationLocaleConsistency } from "./i18n-keys.js";
|
|
18
|
+
import { validateI18nSurfaceKeys, validateRequiredLocales, validateTranslationLocaleConsistency, } from "./i18n-keys.js";
|
|
19
19
|
import { collectKnownRoles, collectNavQns, collectWriteHandlerQns, validateNavCycles, validateNavs, warnOnNavAccessInversion, warnOnUnreachableNavScreens, } from "./nav.js";
|
|
20
20
|
import { collectClaimKeys, validateOwnershipRules } from "./ownership.js";
|
|
21
21
|
import { validateParentRefs } from "./parent-ref.js";
|
|
@@ -219,6 +219,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
219
219
|
validateDefaultWorkspaceUniqueness(allWorkspaceQns);
|
|
220
220
|
validateI18nSurfaceKeys(features);
|
|
221
221
|
validateTranslationLocaleConsistency(features);
|
|
222
|
+
validateRequiredLocales(features, options?.requiredLocales);
|
|
222
223
|
validateEntityListScreens(features);
|
|
223
224
|
validateSubmitPrefilledHiddenFields(features);
|
|
224
225
|
validateCardMetaOverflow(features);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
2
|
+
import type { FeatureDefinition } from "../types/index.ts";
|
|
3
|
+
import type { ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessLink, ProjectionDetailScreenDefinition, RelatedListToolbarAction, RowAction, RowActionNavigateBase, RowFieldExtractor, ScreenDefinition, SecretMintScreenDefinition, ToolbarAction } from "../types/screen.ts";
|
|
4
|
+
export declare function isRowClickAction(action: RowAction): boolean;
|
|
5
|
+
export declare function validateAtMostOneRowClick(featureName: string, screenId: string, screenType: "entityList" | "projectionList", rowActions: readonly RowAction[]): void;
|
|
6
|
+
export declare function rowFieldExtractorKeys(params: RowFieldExtractor): readonly string[];
|
|
7
|
+
export declare function readsNavigateParamsAsFormPrefill(target: ScreenDefinition, explicitEntityId: string | undefined, sourceScreenEntity: string | undefined): boolean;
|
|
8
|
+
export type NavigateSourceScreenType = "dashboard" | "entityList" | "projectionList" | "projectionDetail" | "entityEdit" | "actionForm" | "secretMint" | "entityList expandableRow";
|
|
9
|
+
export declare function validateRowActionNavigateParams(featureName: string, screenId: string, screenType: NavigateSourceScreenType, screenEntity: string | undefined, action: RowAction, target: {
|
|
10
|
+
readonly featureName: string;
|
|
11
|
+
readonly screen: ScreenDefinition;
|
|
12
|
+
} | undefined, actionLabel?: "rowAction" | "emptyState action"): void;
|
|
13
|
+
export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList" | "dashboard", context: string, action: {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly icon?: IconKey;
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function validateActionFieldRefs(featureName: string, screenId: string, actionKind: "rowAction" | "toolbarAction", actionId: string, action: RowAction | ToolbarAction | RelatedListToolbarAction, fieldNames: ReadonlySet<string>, rowMeta: ReadonlySet<string>): void;
|
|
18
|
+
export declare function validateScreenShortIdCollisions(screensByShortId: ReadonlyMap<string, ReadonlyArray<{
|
|
19
|
+
readonly featureName: string;
|
|
20
|
+
readonly screen: ScreenDefinition;
|
|
21
|
+
}>>): void;
|
|
22
|
+
export declare function resolveScreenTargetQn(featureName: string, target: string): string;
|
|
23
|
+
export declare function validateScreenNavTarget(featureName: string, screenId: string, screenKind: string, fieldName: string, value: string, allScreenQns: ReadonlySet<string>, screens: FeatureDefinition["screens"]): void;
|
|
24
|
+
export declare function validateDrawerTargetAction(featureName: string, screenId: string, screenKind: string, actionLabel: "toolbarAction" | "rowAction" | "emptyState action" | "action", action: {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly screen: string;
|
|
27
|
+
readonly params?: RowFieldExtractor;
|
|
28
|
+
}, screens: FeatureDefinition["screens"]): void;
|
|
29
|
+
export declare function isSensitiveOrPasswordField(field: object | undefined): boolean;
|
|
30
|
+
export declare function validateDrawerPrefillKeys(where: string, targetLabel: "drawer-target" | "navigate-target", targetScreenId: string, target: ActionFormScreenDefinition | SecretMintScreenDefinition, params: RowFieldExtractor): void;
|
|
31
|
+
type RowActionNavigateRuntime = RowActionNavigateBase & {
|
|
32
|
+
readonly screen?: string;
|
|
33
|
+
readonly entity?: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveRowActionNavigateTarget(featureName: string, screenId: string, screenType: NavigateSourceScreenType, actionLabel: string, action: RowActionNavigateRuntime, allScreenQns: ReadonlySet<string>, navTargetShortIds: ReadonlySet<string>, screensByShortId: ReadonlyMap<string, ReadonlyArray<{
|
|
36
|
+
readonly featureName: string;
|
|
37
|
+
readonly screen: ScreenDefinition;
|
|
38
|
+
}>>, detailForScreens: ReadonlyMap<string, {
|
|
39
|
+
readonly featureName: string;
|
|
40
|
+
readonly screen: ScreenDefinition;
|
|
41
|
+
}>): {
|
|
42
|
+
readonly featureName: string;
|
|
43
|
+
readonly screen: ScreenDefinition;
|
|
44
|
+
} | undefined;
|
|
45
|
+
export declare function validateRedirectTarget(feature: FeatureDefinition, screenId: string, screenKind: "actionForm" | "secretMint" | "entityEdit" | "projectionDetail", redirect: string | ActionFormRedirect, lookups: NavigateTargetLookups, propertyPath?: string): void;
|
|
46
|
+
export declare function validateIdlessSuccessLink(feature: FeatureDefinition, screenId: string, screenKind: "actionForm", link: ActionFormSuccessLink, lookups: NavigateTargetLookups, propertyPath: string): void;
|
|
47
|
+
export declare function validateClientActionName(featureName: string, screenId: string, screenKind: string, actionLabel: "rowAction" | "emptyState action" | "action", action: Extract<RowAction, {
|
|
48
|
+
readonly kind: "clientAction";
|
|
49
|
+
}>): void;
|
|
50
|
+
export declare function rejectRedirectOnListRowAction(featureName: string, screenId: string, screenKind: string, action: RowAction): void;
|
|
51
|
+
export declare function validateRecordActionRedirect(feature: FeatureDefinition, screenId: string, screenKind: "projectionDetail" | "entityEdit", action: RowAction, lookups: NavigateTargetLookups): void;
|
|
52
|
+
export declare function validatePrimaryActionTarget(featureName: string, screenId: string, screen: ProjectionDetailScreenDefinition): void;
|
|
53
|
+
export declare function validateScreenVisibleWhen(feature: FeatureDefinition, screenId: string, screen: ScreenDefinition, allScreenQns: ReadonlySet<string>, featureMap: ReadonlyMap<string, FeatureDefinition>): void;
|
|
54
|
+
export type NavigateTargetLookups = {
|
|
55
|
+
readonly allScreenQns: ReadonlySet<string>;
|
|
56
|
+
readonly navTargetShortIds: ReadonlySet<string>;
|
|
57
|
+
readonly screensByShortId: ReadonlyMap<string, ReadonlyArray<{
|
|
58
|
+
readonly featureName: string;
|
|
59
|
+
readonly screen: ScreenDefinition;
|
|
60
|
+
}>>;
|
|
61
|
+
readonly detailForScreens: ReadonlyMap<string, {
|
|
62
|
+
readonly featureName: string;
|
|
63
|
+
readonly screen: ScreenDefinition;
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
export declare function validateNavigateActionTab(featureName: string, screenId: string, screenType: NavigateSourceScreenType, actionLabel: string, action: RowAction, lookups: NavigateTargetLookups): void;
|
|
67
|
+
export {};
|