@pramen/server 0.0.22 → 0.0.24
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/durable-object.d.ts +1 -0
- package/dist/durable-object.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/pramen.d.ts +5 -1
- package/dist/sdk/handlers.d.ts +21 -0
- package/dist/worker.js +19 -0
- package/package.json +1 -1
- package/src/durable-object.ts +26 -0
- package/src/index.ts +1 -1
- package/src/pramen.ts +5 -1
- package/src/sdk/handlers.ts +23 -0
- package/src/worker.ts +19 -0
package/dist/durable-object.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
45
45
|
private readonly subsBySocket;
|
|
46
46
|
constructor(ctx: DurableObjectState, env: DoEnv, app: PramenApp);
|
|
47
47
|
private ensureMigrated;
|
|
48
|
+
private runBootstrap;
|
|
48
49
|
fetch(request: Request): Promise<Response>;
|
|
49
50
|
/** Arm the drain alarm soon after a mutation enqueued task(s). setAlarm replaces any
|
|
50
51
|
* pending alarm; a near-future time batches a burst of enqueues into one drain. */
|
package/dist/durable-object.js
CHANGED
|
@@ -95,9 +95,32 @@ export class PramenDOBase extends DurableObject {
|
|
|
95
95
|
return; // a concurrent first request already migrated
|
|
96
96
|
await this.driver.transaction(() => migrate(this.driver, this.app.schema, { allowDestructive, partition }).then(() => { }));
|
|
97
97
|
await ensureOutbox(this.driver); // the deferred-tasks table (internal, all partitions)
|
|
98
|
+
await this.runBootstrap(); // converge code-defined reference data (default partition only)
|
|
98
99
|
this.migrated = true;
|
|
99
100
|
});
|
|
100
101
|
}
|
|
102
|
+
// Run app.bootstrap() once per DO lifetime, right after migration and inside the same
|
|
103
|
+
// blockConcurrencyWhile block, so the first request sees a converged store and two
|
|
104
|
+
// concurrent first fetches can't double-run it. Default partition ONLY: reference data
|
|
105
|
+
// (content types, roles, …) lives in the default partition, and a non-default DO doesn't
|
|
106
|
+
// own those tables (a write would trip assertInPartition). A failing reconciler is logged
|
|
107
|
+
// and swallowed — unlike migrate(), it must never brick a tenant's boot; it retries next
|
|
108
|
+
// boot. Uses a SYSTEM-scoped Db (ACL bypassed) with triggers suppressed (a boot-time seed
|
|
109
|
+
// shouldn't fan out reactive side-effects).
|
|
110
|
+
async runBootstrap() {
|
|
111
|
+
const fns = this.app.bootstrap;
|
|
112
|
+
if (!fns?.length || this.partition !== DEFAULT_PARTITION)
|
|
113
|
+
return;
|
|
114
|
+
const db = new Db(this.driver, { acl: this.acl, identity: { roles: ["admin"] }, system: true, partition: this.partition, schema: this.app.schema, suppressTriggers: true }, this.app.schema);
|
|
115
|
+
for (const fn of fns) {
|
|
116
|
+
try {
|
|
117
|
+
await this.driver.transaction(() => Promise.resolve(fn({ db, driver: this.driver, schema: this.app.schema, partition: this.partition })));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.error(`[pramen] bootstrap failed (partition=${this.partition}):`, e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
101
124
|
async fetch(request) {
|
|
102
125
|
const tenantHeader = request.headers.get("x-pramen-tenant");
|
|
103
126
|
if (tenantHeader)
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { isValidUuid } from "./sdk/uuid";
|
|
|
4
4
|
export type { DefaultValue, FieldType, FieldDef, EntityFields, EntityDef, SchemaDef, RelationDef, RelationDefs, BelongsToDef, HasManyDef, } 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 } from "./sdk/handlers";
|
|
7
|
+
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
8
8
|
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
9
9
|
export type { Action, Identity, IdentityMarker, InputMarker, Policy, PolicyRule, PolicyRules, Role, Validator, WhereRule, ConditionalFields, FieldsFn, RelationAclRule, SetValue, ResolverFn, ResolverContext, ResolverDb, } from "./sdk/acl";
|
|
10
10
|
export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
|
package/dist/pramen.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Env } from "./worker";
|
|
2
2
|
import { pramenDO, type DoEnv } from "./durable-object";
|
|
3
3
|
import { type SchemaDef } from "./sdk/schema";
|
|
4
|
-
import type { AppTaskMap, HandlerMap } from "./sdk/handlers";
|
|
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
7
|
/** Injected into a public route's handler — forward a privileged mutation into the
|
|
@@ -41,6 +41,10 @@ export interface PramenApp {
|
|
|
41
41
|
* `ctx.queue.send(...)`. Dispatched by `createPramen(app).queue` (a consumer is
|
|
42
42
|
* Worker-level: no `ctx.db`, reach a tenant via `ctx.callPrivileged`). */
|
|
43
43
|
queues?: AppQueueMap;
|
|
44
|
+
/** Idempotent reconcilers run once after schema migration on each boot — converge
|
|
45
|
+
* code-defined reference data into the store (see `BootstrapFn`). Each runs with a
|
|
46
|
+
* privileged system Db; failures are logged, never fatal. */
|
|
47
|
+
bootstrap?: readonly BootstrapFn[];
|
|
44
48
|
}
|
|
45
49
|
export type { Env, DoEnv };
|
|
46
50
|
/** Build the deployable pair for an app. `scheduled` is a Cron Trigger entry that
|
package/dist/sdk/handlers.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Db } from "../runtime/db";
|
|
2
|
+
import type { Driver } from "../runtime/driver";
|
|
2
3
|
import type { Kv } from "../runtime/kv";
|
|
3
4
|
import type { Mail } from "../runtime/mail";
|
|
4
5
|
import type { Queue } from "../runtime/queue";
|
|
@@ -39,6 +40,26 @@ export interface HandlerContext<S extends SchemaDef = SchemaDef> {
|
|
|
39
40
|
* Declare queues in oblaka.ts; consume them via `app.queues`. */
|
|
40
41
|
readonly queue: Queue;
|
|
41
42
|
}
|
|
43
|
+
/** Context handed to each `app.bootstrap` function. A privileged, SYSTEM-scoped `Db` (ACL
|
|
44
|
+
* bypassed) plus the raw driver, available once schema migration has run on boot. Use it to
|
|
45
|
+
* reconcile CODE-DEFINED reference data — content types, block types, roles, feature flags —
|
|
46
|
+
* into the store, so a fresh / reprovisioned database converges to what the repo declares
|
|
47
|
+
* instead of depending on rows someone created by hand. */
|
|
48
|
+
export interface BootstrapContext<S extends SchemaDef = SchemaDef> {
|
|
49
|
+
/** System-scoped Db (ACL bypassed), scoped to `partition`. */
|
|
50
|
+
readonly db: Db<S>;
|
|
51
|
+
/** Raw driver — for `driver.transaction(...)` or bespoke SQL. */
|
|
52
|
+
readonly driver: Driver;
|
|
53
|
+
readonly schema: S;
|
|
54
|
+
/** The partition being booted. On the DO path bootstrap runs ONLY for the default
|
|
55
|
+
* partition (reference data lives there); on the D1 path it is always the default. */
|
|
56
|
+
readonly partition: string;
|
|
57
|
+
}
|
|
58
|
+
/** An idempotent reconcile run once after `migrate()` on each boot (a DO's first fetch, or a
|
|
59
|
+
* Worker/D1 isolate init). It MUST be safe to run repeatedly — upsert by a stable key, never
|
|
60
|
+
* blind-insert. A thrown error is logged and swallowed so a broken reconcile can't brick a
|
|
61
|
+
* tenant's boot; it simply retries on the next boot. Set as `app.bootstrap`. */
|
|
62
|
+
export type BootstrapFn = (ctx: BootstrapContext) => void | Promise<void>;
|
|
42
63
|
/** The deferred-side-effects facade handed to handlers as `ctx.tasks`. */
|
|
43
64
|
export interface Tasks {
|
|
44
65
|
/** Enqueue a task to run after commit. `kind` selects the `app.tasks` handler;
|
package/dist/worker.js
CHANGED
|
@@ -129,11 +129,30 @@ export function makeWorker(app) {
|
|
|
129
129
|
// its own). Schema migration over D1 runs once per isolate (and short-circuits on a
|
|
130
130
|
// stored schema hash thereafter); a failed run is not cached.
|
|
131
131
|
const d1Acl = compileAcl(app.acl ?? []);
|
|
132
|
+
// Converge code-defined reference data on the D1 path — the mirror of the DO's
|
|
133
|
+
// runBootstrap(), run once per isolate after migration. D1 is a single shared store (no
|
|
134
|
+
// partition split), so every reconciler runs under the default partition. SYSTEM-scoped
|
|
135
|
+
// Db (ACL bypassed), triggers suppressed; a failing reconciler is logged, never fatal.
|
|
136
|
+
const runBootstrapD1 = async (driver) => {
|
|
137
|
+
const fns = app.bootstrap;
|
|
138
|
+
if (!fns?.length)
|
|
139
|
+
return;
|
|
140
|
+
const db = new Db(driver, { acl: d1Acl, identity: { roles: ["admin"] }, system: true, schema: app.schema, suppressTriggers: true }, app.schema);
|
|
141
|
+
for (const fn of fns) {
|
|
142
|
+
try {
|
|
143
|
+
await driver.transaction(() => Promise.resolve(fn({ db, driver, schema: app.schema, partition: DEFAULT_PARTITION })));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
console.error("[pramen] bootstrap failed (d1):", e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
132
150
|
let d1Ready;
|
|
133
151
|
const ensureD1Migrated = (driver, allowDestructive) => {
|
|
134
152
|
if (!d1Ready) {
|
|
135
153
|
d1Ready = migrate(driver, app.schema, { allowDestructive })
|
|
136
154
|
.then(() => ensureOutbox(driver)) // the deferred-tasks table also lives in D1
|
|
155
|
+
.then(() => runBootstrapD1(driver)) // converge code-defined reference data
|
|
137
156
|
.then(() => undefined)
|
|
138
157
|
.catch((e) => {
|
|
139
158
|
d1Ready = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "pramen server runtime — schema, ACL, ORM, live queries, files, and the createPramen(app) factory for Cloudflare Workers + Durable Objects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/durable-object.ts
CHANGED
|
@@ -133,10 +133,36 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
133
133
|
migrate(this.driver, this.app.schema, { allowDestructive, partition }).then(() => {}),
|
|
134
134
|
);
|
|
135
135
|
await ensureOutbox(this.driver); // the deferred-tasks table (internal, all partitions)
|
|
136
|
+
await this.runBootstrap(); // converge code-defined reference data (default partition only)
|
|
136
137
|
this.migrated = true;
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// Run app.bootstrap() once per DO lifetime, right after migration and inside the same
|
|
142
|
+
// blockConcurrencyWhile block, so the first request sees a converged store and two
|
|
143
|
+
// concurrent first fetches can't double-run it. Default partition ONLY: reference data
|
|
144
|
+
// (content types, roles, …) lives in the default partition, and a non-default DO doesn't
|
|
145
|
+
// own those tables (a write would trip assertInPartition). A failing reconciler is logged
|
|
146
|
+
// and swallowed — unlike migrate(), it must never brick a tenant's boot; it retries next
|
|
147
|
+
// boot. Uses a SYSTEM-scoped Db (ACL bypassed) with triggers suppressed (a boot-time seed
|
|
148
|
+
// shouldn't fan out reactive side-effects).
|
|
149
|
+
private async runBootstrap(): Promise<void> {
|
|
150
|
+
const fns = this.app.bootstrap;
|
|
151
|
+
if (!fns?.length || this.partition !== DEFAULT_PARTITION) return;
|
|
152
|
+
const db = new Db(
|
|
153
|
+
this.driver,
|
|
154
|
+
{ acl: this.acl, identity: { roles: ["admin"] }, system: true, partition: this.partition, schema: this.app.schema, suppressTriggers: true },
|
|
155
|
+
this.app.schema,
|
|
156
|
+
);
|
|
157
|
+
for (const fn of fns) {
|
|
158
|
+
try {
|
|
159
|
+
await this.driver.transaction(() => Promise.resolve(fn({ db, driver: this.driver, schema: this.app.schema, partition: this.partition })));
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error(`[pramen] bootstrap failed (partition=${this.partition}):`, e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
override async fetch(request: Request): Promise<Response> {
|
|
141
167
|
const tenantHeader = request.headers.get("x-pramen-tenant");
|
|
142
168
|
if (tenantHeader) this.tenant = tenantHeader;
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type {
|
|
|
27
27
|
// --- app + handlers ---
|
|
28
28
|
export { createApp } from "./sdk/app";
|
|
29
29
|
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
30
|
-
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap } from "./sdk/handlers";
|
|
30
|
+
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
31
31
|
|
|
32
32
|
// --- ACL ---
|
|
33
33
|
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
package/src/pramen.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { makeWorker, type Env } from "./worker";
|
|
16
16
|
import { pramenDO, type DoEnv } from "./durable-object";
|
|
17
17
|
import { validateTriggerTasks, type SchemaDef } from "./sdk/schema";
|
|
18
|
-
import type { AppTaskMap, HandlerMap } from "./sdk/handlers";
|
|
18
|
+
import type { AppTaskMap, HandlerMap, BootstrapFn } from "./sdk/handlers";
|
|
19
19
|
import type { AppQueueMap, QueueBatch } from "./runtime/queue-consumer";
|
|
20
20
|
import type { Role } from "./sdk/acl";
|
|
21
21
|
|
|
@@ -53,6 +53,10 @@ export interface PramenApp {
|
|
|
53
53
|
* `ctx.queue.send(...)`. Dispatched by `createPramen(app).queue` (a consumer is
|
|
54
54
|
* Worker-level: no `ctx.db`, reach a tenant via `ctx.callPrivileged`). */
|
|
55
55
|
queues?: AppQueueMap;
|
|
56
|
+
/** Idempotent reconcilers run once after schema migration on each boot — converge
|
|
57
|
+
* code-defined reference data into the store (see `BootstrapFn`). Each runs with a
|
|
58
|
+
* privileged system Db; failures are logged, never fatal. */
|
|
59
|
+
bootstrap?: readonly BootstrapFn[];
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export type { Env, DoEnv };
|
package/src/sdk/handlers.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// rolls back on throw (see runtime/dispatch.ts).
|
|
4
4
|
|
|
5
5
|
import type { Db } from "../runtime/db";
|
|
6
|
+
import type { Driver } from "../runtime/driver";
|
|
6
7
|
import type { Kv } from "../runtime/kv";
|
|
7
8
|
import type { Mail } from "../runtime/mail";
|
|
8
9
|
import type { Queue } from "../runtime/queue";
|
|
@@ -45,6 +46,28 @@ export interface HandlerContext<S extends SchemaDef = SchemaDef> {
|
|
|
45
46
|
readonly queue: Queue;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Context handed to each `app.bootstrap` function. A privileged, SYSTEM-scoped `Db` (ACL
|
|
50
|
+
* bypassed) plus the raw driver, available once schema migration has run on boot. Use it to
|
|
51
|
+
* reconcile CODE-DEFINED reference data — content types, block types, roles, feature flags —
|
|
52
|
+
* into the store, so a fresh / reprovisioned database converges to what the repo declares
|
|
53
|
+
* instead of depending on rows someone created by hand. */
|
|
54
|
+
export interface BootstrapContext<S extends SchemaDef = SchemaDef> {
|
|
55
|
+
/** System-scoped Db (ACL bypassed), scoped to `partition`. */
|
|
56
|
+
readonly db: Db<S>;
|
|
57
|
+
/** Raw driver — for `driver.transaction(...)` or bespoke SQL. */
|
|
58
|
+
readonly driver: Driver;
|
|
59
|
+
readonly schema: S;
|
|
60
|
+
/** The partition being booted. On the DO path bootstrap runs ONLY for the default
|
|
61
|
+
* partition (reference data lives there); on the D1 path it is always the default. */
|
|
62
|
+
readonly partition: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** An idempotent reconcile run once after `migrate()` on each boot (a DO's first fetch, or a
|
|
66
|
+
* Worker/D1 isolate init). It MUST be safe to run repeatedly — upsert by a stable key, never
|
|
67
|
+
* blind-insert. A thrown error is logged and swallowed so a broken reconcile can't brick a
|
|
68
|
+
* tenant's boot; it simply retries on the next boot. Set as `app.bootstrap`. */
|
|
69
|
+
export type BootstrapFn = (ctx: BootstrapContext) => void | Promise<void>;
|
|
70
|
+
|
|
48
71
|
/** The deferred-side-effects facade handed to handlers as `ctx.tasks`. */
|
|
49
72
|
export interface Tasks {
|
|
50
73
|
/** Enqueue a task to run after commit. `kind` selects the `app.tasks` handler;
|
package/src/worker.ts
CHANGED
|
@@ -191,11 +191,30 @@ export function makeWorker(app: PramenApp) {
|
|
|
191
191
|
// its own). Schema migration over D1 runs once per isolate (and short-circuits on a
|
|
192
192
|
// stored schema hash thereafter); a failed run is not cached.
|
|
193
193
|
const d1Acl = compileAcl(app.acl ?? []);
|
|
194
|
+
|
|
195
|
+
// Converge code-defined reference data on the D1 path — the mirror of the DO's
|
|
196
|
+
// runBootstrap(), run once per isolate after migration. D1 is a single shared store (no
|
|
197
|
+
// partition split), so every reconciler runs under the default partition. SYSTEM-scoped
|
|
198
|
+
// Db (ACL bypassed), triggers suppressed; a failing reconciler is logged, never fatal.
|
|
199
|
+
const runBootstrapD1 = async (driver: Driver): Promise<void> => {
|
|
200
|
+
const fns = app.bootstrap;
|
|
201
|
+
if (!fns?.length) return;
|
|
202
|
+
const db = new Db(driver, { acl: d1Acl, identity: { roles: ["admin"] }, system: true, schema: app.schema, suppressTriggers: true }, app.schema);
|
|
203
|
+
for (const fn of fns) {
|
|
204
|
+
try {
|
|
205
|
+
await driver.transaction(() => Promise.resolve(fn({ db, driver, schema: app.schema, partition: DEFAULT_PARTITION })));
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error("[pramen] bootstrap failed (d1):", e);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
194
212
|
let d1Ready: Promise<void> | undefined;
|
|
195
213
|
const ensureD1Migrated = (driver: Driver, allowDestructive: boolean): Promise<void> => {
|
|
196
214
|
if (!d1Ready) {
|
|
197
215
|
d1Ready = migrate(driver, app.schema, { allowDestructive })
|
|
198
216
|
.then(() => ensureOutbox(driver)) // the deferred-tasks table also lives in D1
|
|
217
|
+
.then(() => runBootstrapD1(driver)) // converge code-defined reference data
|
|
199
218
|
.then(() => undefined)
|
|
200
219
|
.catch((e) => {
|
|
201
220
|
d1Ready = undefined;
|