@pramen/server 0.0.47 → 0.0.49
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/dist/auth.d.ts +4 -3
- package/dist/cli.js +15 -9
- package/dist/durable-object.d.ts +1 -0
- package/dist/durable-object.js +10 -5
- package/dist/index.d.ts +3 -3
- package/dist/pramen.d.ts +4 -2
- package/dist/runtime/acl.d.ts +6 -5
- package/dist/runtime/acl.js +1 -5
- package/dist/runtime/db.d.ts +3 -2
- package/dist/runtime/dispatch.d.ts +3 -1
- package/dist/runtime/driver.d.ts +8 -5
- package/dist/runtime/mail.d.ts +2 -1
- package/dist/runtime/protocol.d.ts +4 -3
- package/dist/runtime/queue-consumer.d.ts +4 -2
- package/dist/runtime/queue.d.ts +3 -2
- package/dist/runtime/read-engine.d.ts +11 -9
- package/dist/runtime/read-engine.js +4 -1
- package/dist/runtime/registry.d.ts +4 -1
- package/dist/runtime/registry.js +0 -3
- package/dist/runtime/schema-diff.d.ts +6 -6
- package/dist/runtime/schema-diff.js +4 -4
- package/dist/sdk/acl.d.ts +18 -11
- package/dist/sdk/handlers.d.ts +9 -3
- package/dist/sdk/infer.d.ts +17 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +16 -10
- package/package.json +1 -1
- package/src/auth.ts +7 -6
- package/src/cli.ts +21 -9
- package/src/durable-object.ts +15 -8
- package/src/index.ts +6 -2
- package/src/pramen.ts +4 -2
- package/src/runtime/acl.ts +37 -31
- package/src/runtime/db.ts +14 -12
- package/src/runtime/dispatch.ts +5 -3
- package/src/runtime/driver.ts +11 -7
- package/src/runtime/mail.ts +2 -1
- package/src/runtime/migrate.ts +2 -1
- package/src/runtime/outbox.ts +2 -1
- package/src/runtime/protocol.ts +5 -3
- package/src/runtime/queue-consumer.ts +4 -2
- package/src/runtime/queue.ts +4 -2
- package/src/runtime/read-engine.ts +27 -21
- package/src/runtime/registry.ts +5 -1
- package/src/runtime/schema-diff.ts +14 -14
- package/src/sdk/acl.ts +29 -14
- package/src/sdk/handlers.ts +10 -3
- package/src/sdk/infer.ts +21 -0
- package/src/worker.ts +20 -11
package/dist/auth.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Identity } from "./sdk/acl";
|
|
2
|
+
import type { JsonObject } from "./sdk/infer";
|
|
2
3
|
/** Verifies a JWT and returns its claims, or null if invalid. Implementations
|
|
3
4
|
* differ only in how they verify the signature. */
|
|
4
5
|
export interface VerifyStrategy {
|
|
5
|
-
verify(token: string): Promise<
|
|
6
|
+
verify(token: string): Promise<JsonObject | null>;
|
|
6
7
|
}
|
|
7
8
|
/** Optional, opt-in claim validation layered on top of signature + exp/nbf. All
|
|
8
9
|
* default OFF (unset) so existing tokens keep verifying; a deployment turns these on
|
|
@@ -22,7 +23,7 @@ export declare class HmacStrategy implements VerifyStrategy {
|
|
|
22
23
|
private readonly secret;
|
|
23
24
|
private readonly opts;
|
|
24
25
|
constructor(secret: string, opts?: VerifyOptions);
|
|
25
|
-
verify(token: string): Promise<
|
|
26
|
+
verify(token: string): Promise<JsonObject | null>;
|
|
26
27
|
}
|
|
27
28
|
/** RS256 verified against a remote JWKS. Public keys are fetched once and cached
|
|
28
29
|
* (TTL); a token with an unknown `kid` triggers one forced refetch to pick up key
|
|
@@ -35,7 +36,7 @@ export declare class JwksStrategy implements VerifyStrategy {
|
|
|
35
36
|
private fetchedAt;
|
|
36
37
|
private inflight;
|
|
37
38
|
constructor(url: string, ttlMs?: number, opts?: VerifyOptions);
|
|
38
|
-
verify(token: string): Promise<
|
|
39
|
+
verify(token: string): Promise<JsonObject | null>;
|
|
39
40
|
private lookup;
|
|
40
41
|
private keyFor;
|
|
41
42
|
private refresh;
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
20
20
|
import { dirname, resolve } from "node:path";
|
|
21
21
|
import { createTableSql } from "./runtime/ddl";
|
|
22
22
|
import { schemaHash } from "./runtime/migrate";
|
|
23
|
-
import {
|
|
23
|
+
import { diffSchemaFingerprint, schemaFingerprint } from "./runtime/schema-diff";
|
|
24
24
|
import { entitiesInPartition, partitionsOf } from "./sdk/schema";
|
|
25
25
|
/** Mint an HS256 JWT — mirrors what a real auth service would issue, for local
|
|
26
26
|
* dev/testing (`pramen token`, and the default token for `schema status`). Signs
|
|
@@ -90,7 +90,7 @@ Usage: pramen <command>
|
|
|
90
90
|
init [dir] scaffold a new project (app.ts + worker.ts + oblaka.ts)
|
|
91
91
|
schema sql print CREATE TABLE statements for the schema
|
|
92
92
|
schema hash print the schema hash
|
|
93
|
-
schema snapshot save the schema
|
|
93
|
+
schema snapshot save the schema fingerprint to .pramen/schema.json
|
|
94
94
|
schema diff compare the schema to the snapshot (safe vs unsafe)
|
|
95
95
|
schema status compare a deployed tenant's schema to the local schema
|
|
96
96
|
[--tenant t] [--url u] [--token jwt]
|
|
@@ -113,20 +113,23 @@ async function schemaCmd(sub) {
|
|
|
113
113
|
if (sub === "snapshot") {
|
|
114
114
|
const { schema } = await loadApp();
|
|
115
115
|
mkdirSync(dirname(snapshotPath), { recursive: true });
|
|
116
|
-
const snap = { hash: schemaHash(schema),
|
|
116
|
+
const snap = { hash: schemaHash(schema), fingerprint: schemaFingerprint(schema) };
|
|
117
117
|
writeFileSync(snapshotPath, JSON.stringify(snap, null, 2) + "\n");
|
|
118
|
-
console.log(`wrote ${snapshotPath} (${Object.keys(snap.
|
|
118
|
+
console.log(`wrote ${snapshotPath} (${Object.keys(snap.fingerprint).length} tables)`);
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
if (sub === "diff") {
|
|
122
122
|
const { schema } = await loadApp();
|
|
123
|
-
const next =
|
|
123
|
+
const next = schemaFingerprint(schema);
|
|
124
124
|
if (!existsSync(snapshotPath)) {
|
|
125
125
|
console.log("no snapshot — run `pramen schema snapshot` to set a baseline.");
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// `fingerprint` was called `shape` before; keep reading an existing snapshot so an
|
|
129
|
+
// upgrade doesn't force a re-baseline.
|
|
130
|
+
const snap = JSON.parse(readFileSync(snapshotPath, "utf8"));
|
|
131
|
+
const prev = snap.fingerprint ?? snap["shape"] ?? {};
|
|
132
|
+
const changes = diffSchemaFingerprint(prev, next);
|
|
130
133
|
if (changes.length === 0) {
|
|
131
134
|
console.log("no changes since snapshot.");
|
|
132
135
|
return;
|
|
@@ -181,7 +184,7 @@ async function schemaCmd(sub) {
|
|
|
181
184
|
console.log(`live: ${live.hash ?? "(none)"}`);
|
|
182
185
|
console.log(`current: ${current}`);
|
|
183
186
|
console.log(upToDate ? "✓ up to date" : "⚠ BEHIND — migrates on the tenant's next boot");
|
|
184
|
-
const want =
|
|
187
|
+
const want = schemaFingerprint(subset);
|
|
185
188
|
for (const table of Object.keys(want)) {
|
|
186
189
|
const liveCols = new Set(live.tables[table] ?? []);
|
|
187
190
|
const missing = Object.keys(want[table].columns).filter((col) => !liveCols.has(col));
|
|
@@ -202,7 +205,10 @@ async function tokenCmd(args) {
|
|
|
202
205
|
fail("token: <sub> required");
|
|
203
206
|
const roles = pos.slice(1);
|
|
204
207
|
const tenants = flag("tenant")?.split(",");
|
|
205
|
-
|
|
208
|
+
const claims = { sub, roles: roles.length ? roles : ["admin"] };
|
|
209
|
+
if (tenants)
|
|
210
|
+
claims.tenants = tenants;
|
|
211
|
+
console.log(await sign(claims));
|
|
206
212
|
}
|
|
207
213
|
function initCmd(args) {
|
|
208
214
|
const dir = resolve(process.cwd(), positionals(args)[0] ?? ".");
|
package/dist/durable-object.d.ts
CHANGED
package/dist/durable-object.js
CHANGED
|
@@ -164,9 +164,10 @@ export class PramenDOBase extends DurableObject {
|
|
|
164
164
|
return new Response(null, { status: 101, webSocket: client });
|
|
165
165
|
}
|
|
166
166
|
const name = new URL(request.url).pathname.replace(/^\/rpc\//, "");
|
|
167
|
-
|
|
167
|
+
// The RPC body is JSON — parse it into the domain type once, here at the boundary.
|
|
168
|
+
let input = null;
|
|
168
169
|
if (request.method === "POST") {
|
|
169
|
-
input = await request.json().catch(() =>
|
|
170
|
+
input = ((await request.json().catch(() => null)) ?? null);
|
|
170
171
|
}
|
|
171
172
|
try {
|
|
172
173
|
const { result, kind, touched, enqueued } = await dispatch(this.app.handlers, this.app.schema, this.driver, this.kv, this.filesFor(this.tenant), this.envBag, this.ctxFor(identity), name, input);
|
|
@@ -291,12 +292,12 @@ export class PramenDOBase extends DurableObject {
|
|
|
291
292
|
await this.ensureMigrated();
|
|
292
293
|
switch (msg.type) {
|
|
293
294
|
case "subscribe":
|
|
294
|
-
return this.onSubscribe(ws, msg.id, msg.name, msg.input);
|
|
295
|
+
return this.onSubscribe(ws, msg.id, msg.name, msg.input ?? null);
|
|
295
296
|
case "unsubscribe":
|
|
296
297
|
this.setSubs(ws, this.getSubs(ws).filter((s) => s.id !== msg.id));
|
|
297
298
|
return;
|
|
298
299
|
case "call":
|
|
299
|
-
return this.onCall(ws, msg.id, msg.name, msg.input);
|
|
300
|
+
return this.onCall(ws, msg.id, msg.name, msg.input ?? null);
|
|
300
301
|
default:
|
|
301
302
|
return this.send(ws, { type: "error", id: "", error: "unknown message type" });
|
|
302
303
|
}
|
|
@@ -536,8 +537,12 @@ export class PramenDOBase extends DurableObject {
|
|
|
536
537
|
}
|
|
537
538
|
// The DO env (bindings + vars + secrets) handed to handlers as ctx.env. Loosely
|
|
538
539
|
// typed at the boundary so handlers can read any var/secret without a DoEnv cast.
|
|
540
|
+
widenedEnv = null;
|
|
539
541
|
get envBag() {
|
|
540
|
-
|
|
542
|
+
// `this.env` is fixed for the DO's lifetime, so widen it once. This getter is read
|
|
543
|
+
// inside the per-subscription live-query loop, where a copy per read would allocate
|
|
544
|
+
// one whole binding bag per subscription on every write.
|
|
545
|
+
return (this.widenedEnv ??= { ...this.env });
|
|
541
546
|
}
|
|
542
547
|
// One Files facade per DO (a DO serves one tenant). Backed by the R2 binding;
|
|
543
548
|
// signing uses FILES_SECRET. Handlers mint signed urls; the bytes never enter here.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ export { isValidUuid } from "./sdk/uuid";
|
|
|
4
4
|
export type { DefaultValue, FieldType, FieldDef, EntityFields, EntityDef, SchemaDef, RelationDef, RelationDefs, BelongsToDef, HasManyDef, ManyToManyDef, OneHasOneDef, OneHasOneInverseDef, OnDelete, } from "./sdk/schema";
|
|
5
5
|
export { createApp } from "./sdk/app";
|
|
6
6
|
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
7
|
-
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
7
|
+
export type { EnvBag, Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
8
8
|
export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
|
|
9
9
|
export type { Action, Identity, IdentityMarker, InputMarker, NowMarker, Policy, PolicyRule, PolicyRules, Role, Validator, WhereRule, ConditionalFields, FieldsFn, RelationAclRule, SetValue, ResolverFn, ResolverContext, ResolverDb, } from "./sdk/acl";
|
|
10
|
-
export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
|
|
10
|
+
export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, JsonObject, SqlValue, CellValue, Row, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
|
|
11
11
|
export { Kv, denySession, allowSession, isSessionDenied } from "./runtime/kv";
|
|
12
12
|
export type { FileRef, Files, SignUploadOpts, SignDownloadOpts, HeadResult } from "./sdk/files";
|
|
13
13
|
export { R2Adapter, MemoryAdapter, createFiles, handleFileRequest } from "./runtime/storage";
|
|
@@ -20,4 +20,4 @@ export { routeQueue, dispatchQueueBatch } from "./runtime/queue-consumer";
|
|
|
20
20
|
export type { QueueContext, QueueHandler, QueueMessage, QueueBatch, AppQueueMap } from "./runtime/queue-consumer";
|
|
21
21
|
export { PramenError, BadRequest, Unauthorized, Forbidden } from "./runtime/errors";
|
|
22
22
|
export { sqliteDialect, postgresDialect, DoSqliteDriver, D1Driver } from "./runtime/driver";
|
|
23
|
-
export type { Driver, Dialect,
|
|
23
|
+
export type { Driver, Dialect, DriverRow } from "./runtime/driver";
|
package/dist/pramen.d.ts
CHANGED
|
@@ -4,13 +4,15 @@ import { type SchemaDef } from "./sdk/schema";
|
|
|
4
4
|
import type { AppTaskMap, HandlerMap, BootstrapFn } from "./sdk/handlers";
|
|
5
5
|
import type { AppQueueMap, QueueBatch } from "./runtime/queue-consumer";
|
|
6
6
|
import type { Role } from "./sdk/acl";
|
|
7
|
+
import type { EnvBag } from "./sdk/handlers";
|
|
8
|
+
import type { JsonValue } from "./sdk/infer";
|
|
7
9
|
/** Injected into a public route's handler — forward a privileged mutation into the
|
|
8
10
|
* tenant's DO without the handler importing any deploy-side code (so app.ts stays
|
|
9
11
|
* authoring-only). The synthetic identity defaults to the admin role. */
|
|
10
12
|
export interface RouteContext {
|
|
11
13
|
callPrivileged(opts: {
|
|
12
14
|
name: string;
|
|
13
|
-
input?:
|
|
15
|
+
input?: JsonValue;
|
|
14
16
|
tenant?: string;
|
|
15
17
|
roles?: string[];
|
|
16
18
|
}): Promise<Response>;
|
|
@@ -25,7 +27,7 @@ export interface PublicRoute {
|
|
|
25
27
|
method: string;
|
|
26
28
|
/** Exact pathname to match (e.g. "/stripe/webhook"). */
|
|
27
29
|
path: string;
|
|
28
|
-
handler: (request: Request, env:
|
|
30
|
+
handler: (request: Request, env: EnvBag, ctx: RouteContext) => Response | Promise<Response>;
|
|
29
31
|
}
|
|
30
32
|
/** The user-facing app: a schema, the handler map, ACL roles, and optional public
|
|
31
33
|
* (pre-auth) routes. `example/app.ts` exports this shape. */
|
package/dist/runtime/acl.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type Action, type FieldsFn, type Identity, type PolicyRule, type ResolverDb, type Role, type Validator } from "../sdk/acl";
|
|
1
|
+
import { type Action, type FieldsFn, type Identity, type PolicyRule, type ResolverDb, type Role, type Validator, type WhereRule } from "../sdk/acl";
|
|
2
2
|
import { type SqlExpr } from "./read-engine";
|
|
3
3
|
import { PramenError } from "./errors";
|
|
4
|
+
import type { Row } from "../sdk/infer";
|
|
4
5
|
import type { SchemaDef } from "../sdk/schema";
|
|
5
6
|
export declare class AclDenied extends PramenError {
|
|
6
7
|
readonly entity: string;
|
|
@@ -78,15 +79,15 @@ export declare const MAX_REL_DEPTH = 5;
|
|
|
78
79
|
* `allowRelations` is false for single-table contexts (cell-level `when`, which is
|
|
79
80
|
* evaluated in memory and cannot do a SQL round-trip): a relation key then raises a
|
|
80
81
|
* clear authoring error instead of emitting a `sub` node that throws at read time. */
|
|
81
|
-
export declare function compileScopedWhere(rule:
|
|
82
|
+
export declare function compileScopedWhere(rule: WhereRule, entity: string, ctx: AclContext, depth?: number, allowRelations?: boolean): SqlExpr;
|
|
82
83
|
export declare function resolveScope(ctx: AclContext, entity: string, action: Action, depth?: number): Scope;
|
|
83
84
|
/** Effective visible fields for one row = base ∪ matching-conditional ∪ fn-output.
|
|
84
85
|
* Returns null (all fields) when the base is null or a resolver grants everything. */
|
|
85
|
-
export declare function effectiveFields(scope: Scope, row:
|
|
86
|
+
export declare function effectiveFields(scope: Scope, row: Row, identity: Identity | null): string[] | null;
|
|
86
87
|
/** Forced values + validators for a write, gathered from matched write policies.
|
|
87
88
|
* `set` values are resolved against the identity; later policies override earlier. */
|
|
88
89
|
export interface WriteRules {
|
|
89
|
-
set:
|
|
90
|
+
set: Row;
|
|
90
91
|
validators: Validator[];
|
|
91
92
|
}
|
|
92
93
|
export declare function resolveWriteRules(ctx: AclContext, entity: string, action: Action): WriteRules;
|
|
@@ -94,4 +95,4 @@ export declare function resolveWriteRules(ctx: AclContext, entity: string, actio
|
|
|
94
95
|
* target's own read scope OR a parent read policy's relation rule with directAccess. */
|
|
95
96
|
export declare function resolveRelationScope(ctx: AclContext, parentEntity: string, relName: string, target: string): Scope;
|
|
96
97
|
/** Project a row to the permitted fields. null = all. */
|
|
97
|
-
export declare function projectRow(row:
|
|
98
|
+
export declare function projectRow(row: Row, fields: string[] | null): Row;
|
package/dist/runtime/acl.js
CHANGED
|
@@ -85,10 +85,6 @@ function getPath(obj, path) {
|
|
|
85
85
|
return path.split(".").reduce((acc, seg) => (acc == null ? undefined : acc[seg]), obj ?? undefined);
|
|
86
86
|
}
|
|
87
87
|
const UNRESOLVED = Symbol("unresolved");
|
|
88
|
-
// Resolve a value that may be an $identity marker (against the caller), an
|
|
89
|
-
// $input marker (against the request input — a capability/by-key grant), or a
|
|
90
|
-
// $now marker (the evaluation instant). An unresolvable marker yields UNRESOLVED,
|
|
91
|
-
// which makes its rule match nothing. $now always resolves.
|
|
92
88
|
function resolveValue(v, identity, input) {
|
|
93
89
|
if (isNowMarker(v))
|
|
94
90
|
return new Date().toISOString();
|
|
@@ -180,7 +176,7 @@ function pkOf(schema, entity) {
|
|
|
180
176
|
* are skipped: they're re-scoped against THEIR own target's read scope downstream.
|
|
181
177
|
* Mirrors Db.assertReadableWhere's recursion for the top-level user `where`. */
|
|
182
178
|
function assertReadableRelationWhere(where, target, fields, ctx) {
|
|
183
|
-
const targetRels =
|
|
179
|
+
const targetRels = ctx.schema?.[target]?.relations ?? {};
|
|
184
180
|
for (const [k, v] of Object.entries(where)) {
|
|
185
181
|
if (k === "AND" || k === "OR") {
|
|
186
182
|
for (const g of v)
|
package/dist/runtime/db.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type AclContext } from "./acl";
|
|
2
|
+
import type { CellValue, Row as SharedRow } from "../sdk/infer";
|
|
2
3
|
import { type AggFn } from "./read-engine";
|
|
3
4
|
import type { Driver } from "./driver";
|
|
4
5
|
import { type EntityFields, type SchemaDef } from "../sdk/schema";
|
|
5
6
|
import type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, RelationsOf, RelationsResult, WhereClause } from "../sdk/infer";
|
|
6
|
-
type Row =
|
|
7
|
+
type Row = SharedRow;
|
|
7
8
|
type Id = string | number | bigint;
|
|
8
9
|
type OrderSpec<S extends SchemaDef, T extends keyof S> = {
|
|
9
10
|
column: keyof FieldsOf<S[T]> & string;
|
|
@@ -215,7 +216,7 @@ export declare class Db<S extends SchemaDef = SchemaDef> {
|
|
|
215
216
|
/** Delete a row by id within scope. Returns whether a row was deleted. */
|
|
216
217
|
delete<T extends keyof S & string>(table: T, id: Id): Promise<boolean>;
|
|
217
218
|
/** Escape hatch — raw SQL, NOT ACL-checked. For system/internal use only. */
|
|
218
|
-
exec(sql: string, ...params:
|
|
219
|
+
exec(sql: string, ...params: CellValue[]): Promise<Row[]>;
|
|
219
220
|
private returningClause;
|
|
220
221
|
private scopeClause;
|
|
221
222
|
}
|
|
@@ -5,6 +5,8 @@ import type { Kv } from "./kv";
|
|
|
5
5
|
import type { Files } from "../sdk/files";
|
|
6
6
|
import type { SchemaDef } from "../sdk/schema";
|
|
7
7
|
import { type AppTaskMap, type HandlerContext, type HandlerKind, type HandlerMap, type Tasks } from "../sdk/handlers";
|
|
8
|
+
import type { EnvBag } from "../sdk/handlers";
|
|
9
|
+
import type { JsonValue } from "../sdk/infer";
|
|
8
10
|
export interface DispatchResult {
|
|
9
11
|
readonly result: unknown;
|
|
10
12
|
readonly kind: HandlerKind;
|
|
@@ -17,4 +19,4 @@ export interface DispatchResult {
|
|
|
17
19
|
export declare function tasksFacade(driver: Driver, onEnqueue?: () => void): Tasks;
|
|
18
20
|
/** Bind `app.tasks` (which take a ctx) into the ctx-free `TaskMap` the drainer calls. */
|
|
19
21
|
export declare function bindTasks(appTasks: AppTaskMap | undefined, ctx: HandlerContext): TaskMap;
|
|
20
|
-
export declare function dispatch(handlers: HandlerMap, schema: SchemaDef, driver: Driver, kv: Kv, files: Files, env:
|
|
22
|
+
export declare function dispatch(handlers: HandlerMap, schema: SchemaDef, driver: Driver, kv: Kv, files: Files, env: EnvBag, acl: AclContext, name: string, input: JsonValue): Promise<DispatchResult>;
|
package/dist/runtime/driver.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/** A raw row exactly as the substrate returns it, before pramen's object↔JSON codec.
|
|
2
|
+
* Distinct from the decoded `Row` handlers see at the `Db` chokepoint. */
|
|
3
|
+
export type DriverRow = Record<string, SqlValue>;
|
|
4
|
+
import type { CellValue, SqlValue } from "../sdk/infer";
|
|
2
5
|
export interface Dialect {
|
|
3
6
|
/** Render an identifier (table/column), quoting as the backend requires. */
|
|
4
7
|
id(name: string): string;
|
|
@@ -7,7 +10,7 @@ export interface Dialect {
|
|
|
7
10
|
/** Whether INSERT/UPDATE/DELETE ... RETURNING is supported (SQLite/Postgres yes; MySQL no). */
|
|
8
11
|
readonly returning: boolean;
|
|
9
12
|
/** Coerce a JS value for binding (e.g. boolean → 0/1 on SQLite). */
|
|
10
|
-
encode(v:
|
|
13
|
+
encode(v: CellValue): CellValue;
|
|
11
14
|
}
|
|
12
15
|
/** Render an identifier as a standard double-quoted name (`"order"`), guarding its
|
|
13
16
|
* shape first. SQLite (DO SQLite + D1) and Postgres all accept double-quoted
|
|
@@ -27,7 +30,7 @@ export interface Driver {
|
|
|
27
30
|
readonly dialect: Dialect;
|
|
28
31
|
/** Run a parameterized statement and return the result rows (empty for writes
|
|
29
32
|
* without RETURNING). Params are already dialect-encoded by the caller. */
|
|
30
|
-
exec(sql: string, params:
|
|
33
|
+
exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
|
|
31
34
|
/** Run `fn` inside a transaction: commit on resolve, roll back on throw. */
|
|
32
35
|
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
33
36
|
/** Run a fixed sequence of write statements ATOMICALLY with FK checks deferred to the
|
|
@@ -46,7 +49,7 @@ export declare class DoSqliteDriver implements Driver {
|
|
|
46
49
|
private readonly storage;
|
|
47
50
|
readonly dialect: Dialect;
|
|
48
51
|
constructor(storage: DurableObjectStorage);
|
|
49
|
-
exec(sql: string, params:
|
|
52
|
+
exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
|
|
50
53
|
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
51
54
|
}
|
|
52
55
|
/** How a D1Driver's session is anchored (passed to `db.withSession`):
|
|
@@ -79,7 +82,7 @@ export declare class D1Driver implements Driver {
|
|
|
79
82
|
constructor(db: D1Database, opts?: {
|
|
80
83
|
start?: D1SessionStart;
|
|
81
84
|
});
|
|
82
|
-
exec(sql: string, params:
|
|
85
|
+
exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
|
|
83
86
|
/** The session's latest bookmark (null before any query). Threaded back to the client
|
|
84
87
|
* via the `x-pramen-d1-bookmark` response header so a subsequent request can anchor a
|
|
85
88
|
* fresh session at it and read its own writes. */
|
package/dist/runtime/mail.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Kv } from "./kv";
|
|
2
|
+
import type { EnvBag } from "../sdk/handlers";
|
|
2
3
|
export interface MailAddress {
|
|
3
4
|
email: string;
|
|
4
5
|
name?: string;
|
|
@@ -77,4 +78,4 @@ export declare class UnconfiguredMailAdapter implements MailAdapter {
|
|
|
77
78
|
* a synthetic dev sender — an EXPLICIT dev opt-in, never the production default.
|
|
78
79
|
* - else → fail closed: a `send` throws (so a missing-MAIL_FROM prod doesn't silently
|
|
79
80
|
* stash security emails in KV). */
|
|
80
|
-
export declare function createMail(env:
|
|
81
|
+
export declare function createMail(env: EnvBag, kv?: Kv): Mail;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { JsonValue } from "../sdk/infer";
|
|
1
2
|
export interface SubscribeMsg {
|
|
2
3
|
type: "subscribe";
|
|
3
4
|
id: string;
|
|
4
5
|
name: string;
|
|
5
|
-
input?:
|
|
6
|
+
input?: JsonValue;
|
|
6
7
|
}
|
|
7
8
|
export interface UnsubscribeMsg {
|
|
8
9
|
type: "unsubscribe";
|
|
@@ -12,7 +13,7 @@ export interface CallMsg {
|
|
|
12
13
|
type: "call";
|
|
13
14
|
id: string;
|
|
14
15
|
name: string;
|
|
15
|
-
input?:
|
|
16
|
+
input?: JsonValue;
|
|
16
17
|
}
|
|
17
18
|
export type ClientMsg = SubscribeMsg | UnsubscribeMsg | CallMsg;
|
|
18
19
|
export type ServerMsg = {
|
|
@@ -32,7 +33,7 @@ export type ServerMsg = {
|
|
|
32
33
|
export interface Subscription {
|
|
33
34
|
id: string;
|
|
34
35
|
name: string;
|
|
35
|
-
input:
|
|
36
|
+
input: JsonValue;
|
|
36
37
|
/** Tables the query read — the coarse prefilter for which writes might matter. */
|
|
37
38
|
tables: string[];
|
|
38
39
|
/** Digest of the last result pushed — used to suppress no-op (row-level) pushes. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Mail } from "./mail";
|
|
2
2
|
import type { Queue } from "./queue";
|
|
3
3
|
import type { Kv } from "./kv";
|
|
4
|
+
import type { EnvBag } from "../sdk/handlers";
|
|
5
|
+
import type { JsonValue } from "../sdk/infer";
|
|
4
6
|
/** One received message (the Cloudflare Queues `Message` shape). */
|
|
5
7
|
export interface QueueMessage<Body = unknown> {
|
|
6
8
|
readonly id: string;
|
|
@@ -30,7 +32,7 @@ export interface QueueBatch<Body = unknown> {
|
|
|
30
32
|
* tenant data via `ctx.callPrivileged`. */
|
|
31
33
|
export interface QueueContext {
|
|
32
34
|
/** The Worker environment (bindings + vars + secrets). */
|
|
33
|
-
readonly env:
|
|
35
|
+
readonly env: EnvBag;
|
|
34
36
|
/** Project KV (cross-tenant). */
|
|
35
37
|
readonly kv: Kv;
|
|
36
38
|
/** Send email (the notification path). */
|
|
@@ -41,7 +43,7 @@ export interface QueueContext {
|
|
|
41
43
|
* The message body should carry the `tenant`. */
|
|
42
44
|
callPrivileged(opts: {
|
|
43
45
|
name: string;
|
|
44
|
-
input?:
|
|
46
|
+
input?: JsonValue;
|
|
45
47
|
tenant?: string;
|
|
46
48
|
roles?: string[];
|
|
47
49
|
partition?: string;
|
package/dist/runtime/queue.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EnvBag } from "../sdk/handlers";
|
|
1
2
|
/** Cloudflare Queues content type for a sent message. Omitted ⇒ the platform default
|
|
2
3
|
* (v8 structured clone). Use "json" for cross-runtime / external consumers. */
|
|
3
4
|
export type QueueContentType = "text" | "bytes" | "json" | "v8";
|
|
@@ -64,9 +65,9 @@ export declare class MemoryQueueAdapter implements QueueAdapter {
|
|
|
64
65
|
/** Discover the Cloudflare Queues producer bindings in an environment: any value that
|
|
65
66
|
* exposes BOTH `send` and `sendBatch` functions (which excludes the email `send`-only
|
|
66
67
|
* binding, KV, R2, D1, the DO namespace, …). Returns name → binding. */
|
|
67
|
-
export declare function discoverQueueBindings(env:
|
|
68
|
+
export declare function discoverQueueBindings(env: EnvBag): Record<string, QueueProducerBinding>;
|
|
68
69
|
/** Build `ctx.queue` from the environment: a Cloudflare adapter over the discovered
|
|
69
70
|
* producer bindings. Sending to an undeclared queue fails closed (the adapter throws).
|
|
70
71
|
* There is no silent capture fallback — declare the `Queue` binding and it exists in
|
|
71
72
|
* dev (lopata) and miniflare too. */
|
|
72
|
-
export declare function createQueue(env:
|
|
73
|
+
export declare function createQueue(env: EnvBag): Queue;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WhereRule } from "../sdk/acl";
|
|
2
|
+
import type { CellValue, Row } from "../sdk/infer";
|
|
1
3
|
import type { Dialect } from "./driver";
|
|
2
4
|
export type CmpOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "LIKE";
|
|
3
5
|
/** Substring match mode for the structured string operators (auto-escaping, so the
|
|
@@ -11,11 +13,11 @@ export type SqlExpr = {
|
|
|
11
13
|
t: "cmp";
|
|
12
14
|
op: CmpOp;
|
|
13
15
|
col: string;
|
|
14
|
-
value:
|
|
16
|
+
value: CellValue;
|
|
15
17
|
} | {
|
|
16
18
|
t: "in";
|
|
17
19
|
col: string;
|
|
18
|
-
values:
|
|
20
|
+
values: CellValue[];
|
|
19
21
|
negate: boolean;
|
|
20
22
|
} | {
|
|
21
23
|
t: "null";
|
|
@@ -45,28 +47,28 @@ export type SqlExpr = {
|
|
|
45
47
|
};
|
|
46
48
|
export declare const TRUE: SqlExpr;
|
|
47
49
|
export declare const FALSE: SqlExpr;
|
|
48
|
-
export declare const cmp: (op: CmpOp, col: string, value:
|
|
50
|
+
export declare const cmp: (op: CmpOp, col: string, value: CellValue) => SqlExpr;
|
|
49
51
|
export declare const isNull: (col: string, negate?: boolean) => SqlExpr;
|
|
50
|
-
export declare const inList: (col: string, values:
|
|
52
|
+
export declare const inList: (col: string, values: CellValue[], negate?: boolean) => SqlExpr;
|
|
51
53
|
export declare const strMatch: (col: string, needle: string, mode: StrMode) => SqlExpr;
|
|
52
54
|
export declare const and: (...parts: SqlExpr[]) => SqlExpr;
|
|
53
55
|
export declare const or: (...parts: SqlExpr[]) => SqlExpr;
|
|
54
56
|
export declare const not: (expr: SqlExpr) => SqlExpr;
|
|
55
57
|
/** Equality (null -> IS NULL). Used by ACL scope building and relation loads. */
|
|
56
|
-
export declare const eq: (col: string, value:
|
|
58
|
+
export declare const eq: (col: string, value: CellValue) => SqlExpr;
|
|
57
59
|
/** Compile a structured user predicate into a SqlExpr.
|
|
58
60
|
* Shapes: { col: value } (eq) | { col: { gt, lt, in, like, isNull, … } } | { AND: [...] } | { OR: [...] } */
|
|
59
|
-
export declare function compileWhere(input:
|
|
61
|
+
export declare function compileWhere(input: WhereRule): SqlExpr;
|
|
60
62
|
export interface CompiledSql {
|
|
61
63
|
readonly sql: string;
|
|
62
|
-
readonly params:
|
|
64
|
+
readonly params: CellValue[];
|
|
63
65
|
}
|
|
64
|
-
export declare function compileExpr(expr: SqlExpr, dialect: Dialect, params?:
|
|
66
|
+
export declare function compileExpr(expr: SqlExpr, dialect: Dialect, params?: CellValue[]): CompiledSql;
|
|
65
67
|
/** Evaluate a compiled predicate against an in-memory row. Mirrors compileExpr's
|
|
66
68
|
* semantics (bound-boolean coercion, NULL compares false, empty-IN, LIKE) so the
|
|
67
69
|
* declarative cell-ACL `when` path can decide per-row field visibility without a
|
|
68
70
|
* round-trip to SQLite. */
|
|
69
|
-
export declare function evalExpr(expr: SqlExpr, row:
|
|
71
|
+
export declare function evalExpr(expr: SqlExpr, row: Row): boolean;
|
|
70
72
|
export interface OrderBy {
|
|
71
73
|
column: string;
|
|
72
74
|
dir?: "asc" | "desc";
|
|
@@ -44,7 +44,10 @@ export function compileWhere(input) {
|
|
|
44
44
|
function columnPredicate(col, v) {
|
|
45
45
|
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
46
46
|
const ops = [];
|
|
47
|
-
for (const [op,
|
|
47
|
+
for (const [op, operand] of Object.entries(v)) {
|
|
48
|
+
// Markers are resolved upstream (runtime/acl.ts), so an operator's operand is a
|
|
49
|
+
// literal cell value by this point.
|
|
50
|
+
const val = operand;
|
|
48
51
|
switch (op) {
|
|
49
52
|
case "eq":
|
|
50
53
|
ops.push(eq(col, val));
|
|
@@ -25,4 +25,7 @@ export declare function parseRegistryKey(key: string): DoRef | null;
|
|
|
25
25
|
/** Enumerate every registered `(tenant, partition)` pair from the registry KV.
|
|
26
26
|
* Paginates over the full listing (cursor / list_complete) — never truncates at the
|
|
27
27
|
* 1000-key page limit. */
|
|
28
|
-
|
|
28
|
+
/** The slice of KV that DO enumeration needs — narrower than the whole namespace, so
|
|
29
|
+
* callers (and test doubles) only have to provide `list`. */
|
|
30
|
+
export type KvLister = Pick<KVNamespace, "list">;
|
|
31
|
+
export declare function listDOs(kv: KvLister): Promise<DoRef[]>;
|
package/dist/runtime/registry.js
CHANGED
|
@@ -65,9 +65,6 @@ export function parseRegistryKey(key) {
|
|
|
65
65
|
return { tenant: rest, partition: DEFAULT_PARTITION };
|
|
66
66
|
return { tenant: rest.slice(0, sep), partition: rest.slice(sep + 1) };
|
|
67
67
|
}
|
|
68
|
-
/** Enumerate every registered `(tenant, partition)` pair from the registry KV.
|
|
69
|
-
* Paginates over the full listing (cursor / list_complete) — never truncates at the
|
|
70
|
-
* 1000-key page limit. */
|
|
71
68
|
export async function listDOs(kv) {
|
|
72
69
|
const out = [];
|
|
73
70
|
let cursor;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SchemaDef } from "../sdk/schema";
|
|
2
2
|
/** The comparable fingerprint of a single column: type + migration-relevant modifiers. */
|
|
3
|
-
export interface
|
|
3
|
+
export interface ColumnFingerprint {
|
|
4
4
|
type: string;
|
|
5
5
|
notNull?: boolean;
|
|
6
6
|
unique?: boolean;
|
|
@@ -11,13 +11,13 @@ export interface ColumnShape {
|
|
|
11
11
|
default?: string;
|
|
12
12
|
}
|
|
13
13
|
/** The comparable fingerprint of a table: its partition + each column's shape. */
|
|
14
|
-
export interface
|
|
14
|
+
export interface TableFingerprint {
|
|
15
15
|
partition: string;
|
|
16
|
-
columns: Record<string,
|
|
16
|
+
columns: Record<string, ColumnFingerprint>;
|
|
17
17
|
}
|
|
18
18
|
/** table -> table shape. The comparable surface of a schema. */
|
|
19
|
-
export type
|
|
20
|
-
export declare function
|
|
19
|
+
export type SchemaFingerprint = Record<string, TableFingerprint>;
|
|
20
|
+
export declare function schemaFingerprint(schema: SchemaDef): SchemaFingerprint;
|
|
21
21
|
export interface SchemaChange {
|
|
22
22
|
kind: "add-table" | "drop-table" | "add-column" | "drop-column" | "change-type" | "change-column" | "move-partition";
|
|
23
23
|
table: string;
|
|
@@ -34,4 +34,4 @@ export interface SchemaChange {
|
|
|
34
34
|
* cross-DO data migration). Reported for honesty. */
|
|
35
35
|
appliesOnBoot: boolean;
|
|
36
36
|
}
|
|
37
|
-
export declare function
|
|
37
|
+
export declare function diffSchemaFingerprint(prev: SchemaFingerprint, next: SchemaFingerprint): SchemaChange[];
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// still CANNOT be enacted on boot (a partition is a separate Durable Object — it needs a
|
|
18
18
|
// manual cross-DO data migration), so it stays `appliesOnBoot: false`.
|
|
19
19
|
import { partitionOf } from "../sdk/schema";
|
|
20
|
-
function
|
|
20
|
+
function columnFingerprint(f) {
|
|
21
21
|
const c = { type: f.type };
|
|
22
22
|
if (f.notNull)
|
|
23
23
|
c.notNull = true;
|
|
@@ -35,12 +35,12 @@ function columnShape(f) {
|
|
|
35
35
|
c.default = JSON.stringify(f.default);
|
|
36
36
|
return c;
|
|
37
37
|
}
|
|
38
|
-
export function
|
|
38
|
+
export function schemaFingerprint(schema) {
|
|
39
39
|
const out = {};
|
|
40
40
|
for (const [table, def] of Object.entries(schema)) {
|
|
41
41
|
const columns = {};
|
|
42
42
|
for (const [col, f] of Object.entries(def.fields))
|
|
43
|
-
columns[col] =
|
|
43
|
+
columns[col] = columnFingerprint(f);
|
|
44
44
|
out[table] = { partition: partitionOf(schema, table), columns };
|
|
45
45
|
}
|
|
46
46
|
return out;
|
|
@@ -64,7 +64,7 @@ function modifierDiff(prev, next) {
|
|
|
64
64
|
function fmt(v) {
|
|
65
65
|
return v === undefined ? "—" : String(v);
|
|
66
66
|
}
|
|
67
|
-
export function
|
|
67
|
+
export function diffSchemaFingerprint(prev, next) {
|
|
68
68
|
const changes = [];
|
|
69
69
|
for (const table of Object.keys(next)) {
|
|
70
70
|
const pt = prev[table];
|