@pramen/server 0.0.1 → 0.0.3

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/src/sdk/infer.ts CHANGED
@@ -62,6 +62,30 @@ export type WhereInput<F extends EntityFields> = {
62
62
  OR?: WhereInput<F>[];
63
63
  };
64
64
 
65
+ // --- relation-aware where (a relation key takes a nested clause over its target,
66
+ // compiled to a security-scoped subquery). Depth-bounded so the type stays finite
67
+ // under cyclic relations (e.g. user.notes ↔ note.owner). The bound matches the
68
+ // runtime MAX_REL_DEPTH in runtime/acl.ts — keep the two in lockstep. ---
69
+
70
+ type PrevDepth = [never, 0, 1, 2, 3, 4, 5];
71
+ type RelTargetTable<S extends SchemaDef, R> = R extends { target: infer Tg } ? (Tg extends keyof S ? Tg : never) : never;
72
+
73
+ /** A `where` clause: column predicates + AND/OR, plus relation keys that take a
74
+ * nested `WhereClause` over the related entity (traversal). */
75
+ export type WhereClause<S extends SchemaDef, T extends keyof S, D extends number = 5> = WhereInput<FieldsOf<S[T]>> &
76
+ ([D] extends [never]
77
+ ? object
78
+ : {
79
+ // Remap away the `string`/`number` index keys so a relationless entity
80
+ // (RelationsOf = Record<string, never>) contributes `{}`, not an index
81
+ // signature that would reject every column key in the WhereInput half.
82
+ [K in keyof RelationsOf<S[T]> as string extends K ? never : number extends K ? never : K]?: WhereClause<
83
+ S,
84
+ RelTargetTable<S, RelationsOf<S[T]>[K]>,
85
+ PrevDepth[D]
86
+ >;
87
+ });
88
+
65
89
  /** Patch input for updates: every column optional, value typed (nullable). */
66
90
  export type InferUpdate<F extends EntityFields> = Partial<{ [K in keyof F]: FieldTsType<F[K]> | null }>;
67
91
 
package/src/worker.ts CHANGED
@@ -171,9 +171,9 @@ export function makeWorker(app: PramenApp) {
171
171
 
172
172
  // --- admin: list known tenants ---
173
173
  if (url.pathname === "/tenants") {
174
- if (!isAdmin(identity)) return forbidden("tenants");
174
+ if (!isAdmin(identity)) return withCors(forbidden("tenants"), cors);
175
175
  const list = await env.KV.list({ prefix: "tenant:" });
176
- return json({ ok: true, result: list.keys.map((k) => k.name.slice("tenant:".length)) });
176
+ return withCors(json({ ok: true, result: list.keys.map((k) => k.name.slice("tenant:".length)) }), cors);
177
177
  }
178
178
 
179
179
  // --- admin: point-in-time recovery for a tenant ---
@@ -193,10 +193,29 @@ export function makeWorker(app: PramenApp) {
193
193
 
194
194
  // --- admin: a tenant's applied schema (hash + tables) ---
195
195
  if (url.pathname === "/admin/schema") {
196
- if (!isAdmin(identity)) return forbidden("schema");
196
+ if (!isAdmin(identity)) return withCors(forbidden("schema"), cors);
197
197
  const tenant = url.searchParams.get("tenant") ?? "main";
198
198
  const stub = env.PRAMEN.get(env.PRAMEN.idFromName(tenant));
199
- return stub.fetch(new Request("https://do/__schema", { headers: { "x-pramen-tenant": tenant } }));
199
+ const res = await stub.fetch(new Request("https://do/__schema", { headers: { "x-pramen-tenant": tenant } }));
200
+ return withCors(res, cors);
201
+ }
202
+
203
+ // --- admin: generic data ops over a tenant's tables (browse/edit any row).
204
+ // Body: { tenant, table, op: list|get|create|update|delete|count, ... }. Runs
205
+ // in the DO under SYSTEM scope (ACL bypassed) — gated to admins here. ---
206
+ if (url.pathname === "/admin/data" && request.method === "POST") {
207
+ if (!isAdmin(identity)) return forbidden("data");
208
+ const body = (await request.json().catch(() => ({}))) as { tenant?: unknown };
209
+ const tenant = typeof body.tenant === "string" && body.tenant ? body.tenant : "main";
210
+ const stub = env.PRAMEN.get(env.PRAMEN.idFromName(tenant));
211
+ const res = await stub.fetch(
212
+ new Request("https://do/__admin/data", {
213
+ method: "POST",
214
+ headers: { "content-type": "application/json", "x-pramen-tenant": tenant },
215
+ body: JSON.stringify(body),
216
+ }),
217
+ );
218
+ return withCors(res, cors);
200
219
  }
201
220
 
202
221
  const isRpc = url.pathname.startsWith("/rpc/");
@@ -206,7 +225,8 @@ export function makeWorker(app: PramenApp) {
206
225
  return new Response(
207
226
  "pramen — POST /rpc/<handler> (JSON body), or WebSocket /live for live queries. " +
208
227
  "Header X-Pramen-Tenant selects the store (default: main). " +
209
- "Admin: GET /tenants, POST /admin/recover {tenant,timestamp}, GET /admin/schema?tenant=.\n",
228
+ "Admin: GET /tenants, POST /admin/recover {tenant,timestamp}, GET /admin/schema?tenant=, " +
229
+ "POST /admin/data {tenant,table,op}.\n",
210
230
  { headers: { "content-type": "text/plain" } },
211
231
  );
212
232
  }