@palbase/backend 23.0.0 → 23.1.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.
Files changed (60) hide show
  1. package/dist/bin/palbase-backend.cjs +313 -24
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +5 -5
  4. package/dist/{chunk-Y5HXVUMP.js → chunk-HQRJDARQ.js} +2 -2
  5. package/dist/{chunk-REZU6UKT.js → chunk-M5MCBWJI.js} +296 -25
  6. package/dist/chunk-M5MCBWJI.js.map +1 -0
  7. package/dist/{chunk-W5ODXPY3.js → chunk-NS5V43YQ.js} +14 -1
  8. package/dist/chunk-NS5V43YQ.js.map +1 -0
  9. package/dist/{chunk-FSGSB42K.js → chunk-OHALWEOG.js} +9 -4
  10. package/dist/chunk-OHALWEOG.js.map +1 -0
  11. package/dist/{chunk-HAF67F2H.js → chunk-PY7YJDCT.js} +57 -3
  12. package/dist/chunk-PY7YJDCT.js.map +1 -0
  13. package/dist/{chunk-ZC6Q2BRD.js → chunk-R3KN6RHD.js} +2 -2
  14. package/dist/{chunk-OMRTHM4X.js → chunk-RCLNBJCM.js} +1 -1
  15. package/dist/chunk-RCLNBJCM.js.map +1 -0
  16. package/dist/db/index.cjs +68 -1
  17. package/dist/db/index.cjs.map +1 -1
  18. package/dist/db/index.d.cts +2 -2
  19. package/dist/db/index.d.ts +2 -2
  20. package/dist/db/index.js +2 -2
  21. package/dist/{endpoint-BavvbW4P.d.ts → endpoint-CVWXh6oG.d.ts} +70 -8
  22. package/dist/{endpoint-i8TTCohk.d.cts → endpoint-c9h5jriX.d.cts} +70 -8
  23. package/dist/engine/index.cjs +313 -24
  24. package/dist/engine/index.cjs.map +1 -1
  25. package/dist/engine/index.d.cts +4 -4
  26. package/dist/engine/index.d.ts +4 -4
  27. package/dist/engine/index.js +4 -4
  28. package/dist/{index-B7YBEG5w.d.ts → index-BZrJXnVh.d.ts} +49 -4
  29. package/dist/{index-Bmvx1EvJ.d.cts → index-By8Dle5U.d.cts} +86 -17
  30. package/dist/{index-B3jmmItD.d.ts → index-CwAJ7HEe.d.ts} +86 -17
  31. package/dist/{index-E7OscPJT.d.cts → index-CxeQSfJP.d.cts} +49 -4
  32. package/dist/index.cjs +104 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +8 -8
  35. package/dist/index.d.ts +8 -8
  36. package/dist/index.js +32 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/openapi/index.cjs +29 -4
  39. package/dist/openapi/index.cjs.map +1 -1
  40. package/dist/openapi/index.d.cts +5 -2
  41. package/dist/openapi/index.d.ts +5 -2
  42. package/dist/openapi/index.js +32 -7
  43. package/dist/openapi/index.js.map +1 -1
  44. package/dist/{registry-DY3d9l1k.d.ts → registry-B3niOVYp.d.ts} +101 -7
  45. package/dist/{registry-C3H2uPeZ.d.cts → registry-CqPK2Qby.d.cts} +101 -7
  46. package/dist/test/index.cjs +482 -9
  47. package/dist/test/index.cjs.map +1 -1
  48. package/dist/test/index.d.cts +35 -3
  49. package/dist/test/index.d.ts +35 -3
  50. package/dist/test/index.js +480 -8
  51. package/dist/test/index.js.map +1 -1
  52. package/package.json +4 -3
  53. package/stager/return_types.js +23 -0
  54. package/dist/chunk-FSGSB42K.js.map +0 -1
  55. package/dist/chunk-HAF67F2H.js.map +0 -1
  56. package/dist/chunk-OMRTHM4X.js.map +0 -1
  57. package/dist/chunk-REZU6UKT.js.map +0 -1
  58. package/dist/chunk-W5ODXPY3.js.map +0 -1
  59. /package/dist/{chunk-Y5HXVUMP.js.map → chunk-HQRJDARQ.js.map} +0 -0
  60. /package/dist/{chunk-ZC6Q2BRD.js.map → chunk-R3KN6RHD.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorators/registry.ts","../src/errors.ts"],"sourcesContent":["// The decorator registry — the single plain-data store the method + parameter\n// decorators write into, and the deploy/dispatch pipeline reads back. No\n// `reflect-metadata`, no `emitDecoratorMetadata`: the registry is built from the\n// decorator arguments + the parameter INDEX that esbuild/tsc preserve for legacy\n// parameter decorators (verified — see the design spec §0/§4.1).\n//\n// A controller class carries its route metadata on a symbol-keyed static\n// property (`ROUTES`). `@Get`/`@Post`/… append a {@link RouteMeta} entry;\n// `@Body`/`@User`/… append a {@link ParamMeta} entry onto the route for the\n// method they decorate. Because parameter decorators run BEFORE the method\n// decorator for the same member (TS evaluates innermost-first, params before the\n// method), the route entry may not exist yet when a param decorator fires — so\n// param metadata is buffered per method name and merged when the method\n// decorator creates the route entry.\nimport type { AuthSpec, RateLimitConfig } from \"../endpoint.js\";\nimport type { UploadConfig } from \"./upload.js\";\nimport type { SseConfig } from \"./sse.js\";\nimport type { ZodTypeAny } from \"zod\";\n\n/** The HTTP verbs a route may declare, upper-cased (the runtime router +\n * OpenAPI lower-case on their own). */\nexport type HttpMethodUpper = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"QUERY\";\n\n/** Route-level options accepted by the method decorators (`@Get`/`@Post`/…). */\nexport interface RouteOptions {\n /** OVERRIDES the controller-level default auth for this one route. */\n auth?: AuthSpec;\n /** Per-route rate limit. */\n rateLimit?: RateLimitConfig;\n /** Direct-storage upload config — present ONLY on `@Upload` routes (the\n * `@Get`/`@Post`/… decorators never set it). Its presence is what MARKS a\n * route as an upload route through the whole pipeline (registry → flatten →\n * openapi → codegen). The bytes go client→storage directly; the method body\n * runs as the completion handler. See {@link UploadConfig} (decorators/upload.ts). */\n uploadConfig?: UploadConfig;\n /** Streaming config — present ONLY on `@Sse` routes (the `@Get`/`@Post`/…\n * decorators never set it). Its presence is what MARKS a route as a streaming\n * route through the whole pipeline (registry → flatten → openapi → codegen),\n * exactly as `uploadConfig` does for uploads — never a special HTTP verb. An\n * `@Sse` route registers POST like any input-bearing route, so the verb cannot\n * carry the distinction. See {@link SseConfig} (decorators/sse.ts). */\n sseConfig?: SseConfig;\n}\n\n/** The kind of value a parameter decorator injects. Drives both dispatch\n * (which request slice to inject) and codegen (which OpenAPI parameter source a\n * schema-bearing kind maps to). */\nexport type ParamKind =\n | \"body\"\n | \"query\"\n | \"param\"\n | \"headers\"\n | \"user\"\n | \"optionalUser\"\n | \"client\"\n | \"requestId\"\n | \"traceId\"\n | \"req\"\n // `@UploadedObject()` — injects the uploaded object (completion input) on an\n // `@Upload` route. No schema (the shape is the fixed UploadedObject type).\n | \"uploadedObject\"\n // `@SseOut()` — injects the frame writer on an `@Sse` route. No schema (the\n // shape is the fixed SseWriter type).\n | \"sseOut\"\n // `@Signal()` — injects the request's AbortSignal, which aborts when the\n // client disconnects. No schema. NOT derivable from `@Req()`: PBRequest\n // carries only request-scoped data and has no signal (endpoint.ts:358-363).\n | \"signal\";\n\n/** One parameter decorator's recorded metadata. `index` is the parameter\n * position esbuild/tsc preserve; `schema` is present for the schema-bearing\n * kinds (`body`/`query`/`headers`); `name` is the path-param name for `param`. */\nexport interface ParamMeta {\n index: number;\n kind: ParamKind;\n /** Zod schema for `body`/`query`/`headers` (validation + codegen source). */\n schema?: ZodTypeAny;\n /** Path-param name for `@Param(\"id\")`. */\n name?: string;\n}\n\n/** One inferred throw site: the error CLASS name (e.g. \"TodoLocked\") and its\n * wire code (e.g. \"todo_locked\"). `status`, `hasData`, and the data JSON schema\n * are NOT carried here — they resolve from the error registry by `code` at\n * extract/openapi time (single source of truth). */\nexport interface ThrowDescriptor {\n name: string;\n code: string;\n}\n\n/** One route's recorded metadata: the verb + subpath + method name + options,\n * the ordered parameter metas, and the resolved return schema (injected by the\n * codegen step — see `returnSchema`). */\nexport interface RouteMeta {\n method: HttpMethodUpper;\n subpath: string;\n fnName: string;\n options: RouteOptions;\n params: ParamMeta[];\n /** Response schema for the route, if any. Derived from the method's RETURN\n * TYPE by codegen and written here via `recordReturn` (a generated top-level\n * IIFE injected per controller), not by an author-written decorator. */\n returnSchema?: ZodTypeAny;\n /** Error classes this route can throw, if inferred. Derived from the method\n * body + service call graph by the deploy stager's throw analysis and written\n * here via `recordThrows` (a generated top-level IIFE injected per controller,\n * the `recordReturn` twin), not by an author-written decorator. */\n throws?: ThrowDescriptor[];\n}\n\n/** Symbol the route metadata list is stored under on a controller class. Using\n * a symbol (not a string key) keeps it off the public structural surface and\n * avoids any chance of an authored property collision. */\nexport const ROUTES: unique symbol = Symbol.for(\"palbase.backend.routes\");\n\n/** Symbol the per-method buffered parameter metas are stored under while a class\n * is being decorated. Parameter decorators fire before the method decorator, so\n * they buffer here keyed by method name; the method decorator drains the buffer\n * into the route entry it creates. */\nconst PARAM_BUFFER: unique symbol = Symbol.for(\"palbase.backend.paramBuffer\");\n\n/** Symbol the per-method buffered return-type schemas are stored under while a\n * class's registry is being populated. The codegen-injected `recordReturn` call\n * can fire before OR after the method decorator; it buffers here keyed by method\n * name and `recordRoute` drains it into the route entry (and `recordReturn`\n * writes through if the route already exists). Buffering on BOTH sides means a\n * fully-formed route entry always carries its return schema — a raw-symbol\n * reader (the runtime) never has to re-merge. */\nconst RETURN_BUFFER: unique symbol = Symbol.for(\"palbase.backend.returnBuffer\");\n\n/** Symbol the per-method buffered throw descriptors are stored under while a\n * class's registry is being populated. The stager-injected `recordThrows` call\n * can fire before OR after the method decorator; it buffers here keyed by method\n * name and `recordRoute` drains it into the route entry (and `recordThrows`\n * writes through if the route already exists). Buffering on BOTH sides means a\n * fully-formed route entry always carries its throw descriptors — a raw-symbol\n * reader (the runtime) never has to re-merge. */\nconst THROWS_BUFFER: unique symbol = Symbol.for(\"palbase.backend.throwsBuffer\");\n\n/** A class constructor carrying the symbol-keyed registry slots. We type the\n * registry-bearing class as this so the decorators can read/write the slots\n * without `any` — a plain `Function` does not carry index signatures. */\ninterface RegistryCarrier {\n [ROUTES]?: RouteMeta[];\n [PARAM_BUFFER]?: Record<string, ParamMeta[]>;\n [RETURN_BUFFER]?: Record<string, ZodTypeAny>;\n [THROWS_BUFFER]?: Record<string, ThrowDescriptor[]>;\n}\n\n/** Coerce a decorated target (class constructor or its prototype) into the\n * registry carrier that owns the slots. Method/param decorators receive the\n * PROTOTYPE as their target; the class decorator receives the constructor. We\n * always anchor the registry on the CONSTRUCTOR so `getRoutes(ctor)` finds it. */\nfunction carrierOf(target: object): RegistryCarrier {\n // For instance-member decorators, `target` is the prototype; its `.constructor`\n // is the class. For a static member or the class decorator, `target` is the\n // constructor already. Resolve to the constructor either way.\n const ctor =\n typeof target === \"function\"\n ? (target as unknown as RegistryCarrier)\n : (((target as { constructor?: unknown }).constructor ??\n target) as unknown as RegistryCarrier);\n return ctor;\n}\n\n/** Get (creating if absent) the own route list for a class constructor. Own —\n * not inherited — so a subclass does not mutate its base's routes. */\nfunction ownRoutes(carrier: RegistryCarrier): RouteMeta[] {\n if (!Object.prototype.hasOwnProperty.call(carrier, ROUTES)) {\n carrier[ROUTES] = [];\n }\n return carrier[ROUTES] as RouteMeta[];\n}\n\n/** Get (creating if absent) the own per-method param buffer for a class. */\nfunction ownParamBuffer(carrier: RegistryCarrier): Record<string, ParamMeta[]> {\n if (!Object.prototype.hasOwnProperty.call(carrier, PARAM_BUFFER)) {\n carrier[PARAM_BUFFER] = {};\n }\n return carrier[PARAM_BUFFER] as Record<string, ParamMeta[]>;\n}\n\n/** Record a route (called by the method decorators). Drains any parameter\n * metas already buffered for `fnName` into the new route entry, then sorts them\n * by parameter index so dispatch can inject positionally. */\nexport function recordRoute(\n target: object,\n fnName: string,\n method: HttpMethodUpper,\n subpath: string,\n options: RouteOptions,\n): void {\n const carrier = carrierOf(target);\n const routes = ownRoutes(carrier);\n const buffer = ownParamBuffer(carrier);\n const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);\n const route: RouteMeta = { method, subpath, fnName, options, params };\n // Drain a buffered return schema (the recordReturn-ran-first ordering) so the\n // route entry is complete the moment it's created — a raw-symbol consumer\n // (the runtime extractor/worker) sees the return schema without re-merging.\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer && returnBuffer[fnName] !== undefined) {\n route.returnSchema = returnBuffer[fnName];\n }\n // Same drain for buffered throw descriptors (the recordThrows-ran-first\n // ordering) — the route entry is complete the moment it's created.\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer && throwsBuffer[fnName] !== undefined) {\n route.throws = throwsBuffer[fnName];\n }\n routes.push(route);\n}\n\n/** Record one parameter decorator (called by `@Body`/`@User`/…). Buffers per\n * method name; the method decorator merges the buffer into the route entry. If\n * the route already exists (method decorator ran first — TS does evaluate the\n * method decorator AFTER its parameter decorators, but we stay order-robust),\n * the meta is also appended directly so neither ordering loses it. */\nexport function recordParam(target: object, fnName: string, meta: ParamMeta): void {\n const carrier = carrierOf(target);\n const buffer = ownParamBuffer(carrier);\n (buffer[fnName] ??= []).push(meta);\n\n // Order-robust: if the route already exists, merge in place + keep sorted.\n const routes = carrier[ROUTES];\n if (routes) {\n const route = routes.find((r) => r.fnName === fnName);\n if (route) {\n route.params.push(meta);\n route.params.sort((a, b) => a.index - b.index);\n }\n }\n}\n\n/** Attach a return schema to the route for `fnName` (called by the codegen\n * injection that reads the method's return type). If the route does not exist\n * yet, the schema is buffered (RETURN_BUFFER) and drained into the route by\n * `recordRoute` when the method decorator runs. */\nexport function recordReturn(target: object, fnName: string, schema: ZodTypeAny): void {\n const carrier = carrierOf(target);\n const routes = carrier[ROUTES];\n const route = routes?.find((r) => r.fnName === fnName);\n if (route) {\n route.returnSchema = schema;\n return;\n }\n if (!Object.prototype.hasOwnProperty.call(carrier, RETURN_BUFFER)) {\n carrier[RETURN_BUFFER] = {};\n }\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer) returnBuffer[fnName] = schema;\n}\n\n/** Attach the inferred throw descriptors to the route for `fnName` (called by\n * the stager-injected IIFE that carries the throw analysis result — the\n * `recordReturn` twin). If the route does not exist yet, the descriptors are\n * buffered (THROWS_BUFFER) and drained into the route by `recordRoute` when the\n * method decorator runs. */\nexport function recordThrows(target: object, fnName: string, throws: ThrowDescriptor[]): void {\n const carrier = carrierOf(target);\n const routes = carrier[ROUTES];\n const route = routes?.find((r) => r.fnName === fnName);\n if (route) {\n route.throws = throws;\n return;\n }\n if (!Object.prototype.hasOwnProperty.call(carrier, THROWS_BUFFER)) {\n carrier[THROWS_BUFFER] = {};\n }\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer) throwsBuffer[fnName] = throws;\n}\n\n/** Read the route metadata for a controller class (the deploy/dispatch entry\n * point). Applies any buffered return schemas + throw descriptors (for the\n * recordReturn/recordThrows-runs-before orderings) and returns a defensive copy\n * so callers cannot mutate the registry.\n */\nexport function getRoutes(ctor: object): RouteMeta[] {\n const carrier = carrierOf(ctor);\n const routes = carrier[ROUTES] ?? [];\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer) {\n for (const route of routes) {\n const buffered = returnBuffer[route.fnName];\n if (buffered && route.returnSchema === undefined) {\n route.returnSchema = buffered;\n }\n }\n }\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer) {\n for (const route of routes) {\n const buffered = throwsBuffer[route.fnName];\n if (buffered && route.throws === undefined) {\n route.throws = buffered;\n }\n }\n }\n return routes.map((r) => ({\n ...r,\n params: r.params.slice(),\n ...(r.throws !== undefined ? { throws: r.throws.slice() } : {}),\n }));\n}\n","/** HTTP error with structured error response format.\n *\n * The base class for the throwable error classes (`PalError`, `Conflict`,\n * `NotFound`, …). Construct one directly with `throw new HttpError(404,\n * \"todo_not_found\", \"No such todo\")`, or throw a named subclass\n * (`throw new NotFound(\"todo not found\")`). The runtime catches any `HttpError`\n * and emits the standard envelope; on the wire (and to iOS) it surfaces as\n * `BackendError.server(code, status, message, requestId)`.\n *\n * The optional `data` field carries a structured payload alongside the\n * standard envelope — for errors that need to ship extra context\n * (e.g. `new Conflict(\"locked\", \"title_locked\", { retryAfter: 30 })`). It rides\n * through to the iOS typed enum's associated value.\n */\n/**\n * The brand that identifies an HttpError ACROSS SDK instances.\n *\n * A process legitimately holds more than one copy of this SDK — the runtime\n * loads the engine from its own node_modules while the tenant's bundle carries\n * an inlined copy, which is why the controller registry and the error registry\n * are both anchored on `Symbol.for`. The one place that did not follow the\n * pattern was the engine's catch: `err instanceof HttpError` compares CLASS\n * IDENTITY, so a `throw new NotFound()` from the bundle's copy did not match\n * the engine's copy and every typed error in every deployed backend degraded to\n * `500 internal_error`. Measured through the edge on a real deploy: a route\n * throwing `NotFound` answered 500 while the runtime's own log printed the\n * error object with `status: 404` right beside it.\n *\n * `Symbol.for` puts this in the cross-realm registry, so every copy of the SDK\n * agrees on it by VALUE rather than by identity.\n */\nexport const HTTP_ERROR_BRAND: unique symbol = Symbol.for(\"palbase.backend.httpError\");\n\n/**\n * Whether a thrown value is an HttpError from ANY copy of this SDK.\n *\n * The shape is checked as well as the brand: the brand says \"this claims to be\n * one of ours\", the fields say the envelope can actually be built from it, and\n * a half-formed object must fall through to the 500 path rather than produce a\n * malformed response.\n */\nexport function isHttpError(err: unknown): err is HttpError {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as Record<PropertyKey, unknown>;\n return (\n e[HTTP_ERROR_BRAND] === true &&\n typeof e.status === \"number\" &&\n typeof e.error === \"string\" &&\n typeof e.errorDescription === \"string\"\n );\n}\n\nexport class HttpError extends Error {\n public readonly status: number;\n public readonly error: string;\n public readonly errorDescription: string;\n public readonly data?: unknown;\n /** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */\n public readonly [HTTP_ERROR_BRAND] = true;\n\n constructor(status: number, error: string, errorDescription: string, data?: unknown) {\n super(errorDescription);\n this.name = \"HttpError\";\n this.status = status;\n this.error = error;\n this.errorDescription = errorDescription;\n if (data !== undefined) {\n this.data = data;\n }\n }\n\n /**\n * Serialize to the standard Palbase error response format.\n * The `requestId` is injected by the runtime layer from the request context.\n * When called without arguments (e.g. JSON.stringify), request_id is omitted.\n * When `data` is set, it is appended as a strict-superset field.\n */\n toJSON(requestId?: string): {\n error: string;\n error_description: string;\n status: number;\n request_id?: string;\n data?: unknown;\n } {\n const result: {\n error: string;\n error_description: string;\n status: number;\n request_id?: string;\n data?: unknown;\n } = {\n error: this.error,\n error_description: this.errorDescription,\n status: this.status,\n };\n if (requestId) {\n result.request_id = requestId;\n }\n if (this.data !== undefined) {\n result.data = this.data;\n }\n return result;\n }\n}\n\n/**\n * Throw with a custom HTTP status + wire code. The general-purpose escape hatch\n * when none of the named classes (`Conflict`/`NotFound`/…) fits.\n *\n * @example\n * throw new PalError(418, \"teapot\", \"I'm a teapot\");\n */\nexport class PalError extends HttpError {\n constructor(status: number, code: string, description: string, data?: unknown) {\n super(status, code, description, data);\n this.name = \"PalError\";\n }\n}\n\n/** Base for the named status classes. Each subclass fixes its HTTP status; the\n * `code` defaults to the class's canonical wire code (overridable), and the\n * `message` defaults to a human-readable label (overridable). */\nabstract class NamedHttpError extends HttpError {\n protected constructor(\n status: number,\n defaultCode: string,\n name: string,\n message?: string,\n code?: string,\n data?: unknown,\n ) {\n super(status, code ?? defaultCode, message ?? defaultMessage(name), data);\n this.name = name;\n }\n}\n\n/** Derive a default human-readable message from a class name\n * (\"NotFound\" → \"Not found\", \"TooManyRequests\" → \"Too many requests\"). */\nfunction defaultMessage(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();\n}\n\n/**\n * 400 — the request was malformed or failed validation. Carries a fixed typed\n * payload: `new BadRequest({ fields: [{ field: \"email\", message: \"invalid\" }] })`.\n * The shape is declared once in the SDK so codegen surfaces `error.data.fields`\n * typed on the client.\n */\nexport class BadRequest extends NamedHttpError {\n public declare readonly data: BadRequestData;\n constructor(data: BadRequestData, message?: string) {\n super(400, \"bad_request\", \"BadRequest\", message, undefined, data);\n }\n}\n\n/** 401 — the caller is not authenticated. */\nexport class Unauthorized extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(401, \"unauthorized\", \"Unauthorized\", message, code, data);\n }\n}\n\n/** 403 — the caller is authenticated but not allowed. */\nexport class Forbidden extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(403, \"forbidden\", \"Forbidden\", message, code, data);\n }\n}\n\n/** 404 — the requested resource does not exist. */\nexport class NotFound extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(404, \"not_found\", \"NotFound\", message, code, data);\n }\n}\n\n/** 409 — the request conflicts with the current state. */\nexport class Conflict extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(409, \"conflict\", \"Conflict\", message, code, data);\n }\n}\n\n/** A single field-level validation failure carried by {@link BadRequest}. */\nexport interface FieldError {\n /** The offending field's name (dotted path for nested fields). */\n field: string;\n /** Human-readable reason the field failed. */\n message: string;\n}\n\n/** The fixed, typed payload {@link BadRequest} ships. */\nexport interface BadRequestData {\n /** The fields that failed validation. */\n fields: FieldError[];\n}\n\n/** The fixed, typed payload {@link TooManyRequests} ships. */\nexport interface TooManyRequestsData {\n /** Seconds the caller should wait before retrying. */\n retryAfter: number;\n}\n\n/**\n * 429 — the caller has exceeded the rate limit. Carries a fixed typed payload:\n * `new TooManyRequests({ retryAfter: 30 })`. The shape is declared once in the\n * SDK (error-registry pre-seed) so codegen surfaces `error.data.retryAfter`\n * typed on the client — no per-project definition needed.\n */\nexport class TooManyRequests extends NamedHttpError {\n public declare readonly data: TooManyRequestsData;\n constructor(data: TooManyRequestsData, message?: string) {\n super(429, \"too_many_requests\", \"TooManyRequests\", message, undefined, data);\n }\n}\n"],"mappings":"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","names":[]}
package/dist/db/index.cjs CHANGED
@@ -47,6 +47,9 @@ __export(db_exports, {
47
47
  module.exports = __toCommonJS(db_exports);
48
48
 
49
49
  // src/db/policy.ts
50
+ function quote(name) {
51
+ return `"${name.replace(/"/g, '""')}"`;
52
+ }
50
53
  var PolicyBuilder = class {
51
54
  _def;
52
55
  constructor(name) {
@@ -82,6 +85,41 @@ var PolicyBuilder = class {
82
85
  this._def.using = sqlExpr;
83
86
  return this;
84
87
  }
88
+ /**
89
+ * "Rows of THIS table whose owner the caller is a member of" — the membership
90
+ * pattern, written so it cannot recurse.
91
+ *
92
+ * THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
93
+ * other: `channels` is visible to members, so its policy reads
94
+ * `channel_members`; `channel_members` is visible to members, so its policy
95
+ * reads `channels`. Postgres refuses the pair at query time with `infinite
96
+ * recursion detected in policy for relation ...`, and the error names the
97
+ * relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
98
+ * is protected by `user_id = auth.uid()` and nothing else, and every other
99
+ * table subqueries INTO it. That shape was in the platform's own schema and
100
+ * written down nowhere; a customer recovered it by reading that schema.
101
+ *
102
+ * `(select auth.uid())` rather than a bare call: the scalar subquery is
103
+ * evaluated ONCE per statement instead of per row.
104
+ *
105
+ * @example
106
+ * // channels: visible to members. The membership table gets the simple one.
107
+ * policy("member_read").for("select").to("authenticated")
108
+ * .memberOf("channel_members", "channel_id")
109
+ * // → id IN (SELECT "channel_id" FROM "channel_members"
110
+ * // WHERE "user_id" = (select auth.uid()))
111
+ */
112
+ memberOf(membershipTable, foreignKey, options = {}) {
113
+ if (this._def.using !== null) {
114
+ throw new Error(
115
+ `policy(${this._def.name}).memberOf(): this policy already has a using() expression. Write one or the other \u2014 memberOf IS the using expression.`
116
+ );
117
+ }
118
+ const column = options.column ?? "id";
119
+ const userColumn = options.userColumn ?? "user_id";
120
+ this._def.using = `${quote(column)} IN (SELECT ${quote(foreignKey)} FROM ${quote(membershipTable)} WHERE ${quote(userColumn)} = (select auth.uid()))`;
121
+ return this;
122
+ }
85
123
  /** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
86
124
  withCheck(sqlExpr) {
87
125
  this._def.withCheck = sqlExpr;
@@ -289,6 +327,21 @@ var ColumnBuilder = class _ColumnBuilder {
289
327
  this._def.defaultNow = true;
290
328
  return new _ColumnBuilder(this._def.type, this._def);
291
329
  }
330
+ /**
331
+ * The DATABASE assigns this column's value — a trigger, a rule, an identity.
332
+ *
333
+ * The column becomes optional on INSERT (the author has nothing to send) while
334
+ * the DDL stays free of a DEFAULT this schema would not honour. It is NOT
335
+ * `default()`: that declares a value the schema promises to write.
336
+ *
337
+ * Naming: deliberately not `generated()`. Postgres has GENERATED columns and
338
+ * they are a different thing; borrowing the word would send a reader — or a
339
+ * model writing a schema — to the wrong feature.
340
+ */
341
+ dbAssigned() {
342
+ this._def.dbAssigned = true;
343
+ return new _ColumnBuilder(this._def.type, this._def);
344
+ }
292
345
  /** Add a foreign key reference. */
293
346
  /**
294
347
  * Declares that this column used to be called `previous`.
@@ -668,6 +721,19 @@ var TxPlanBuilder = class {
668
721
  }
669
722
  return this.push({ op: "insert", table: name, values: encoded }, `${name}.insert()`);
670
723
  },
724
+ upsert: (values, options) => {
725
+ const encoded = encodeMap(values, false);
726
+ if (Object.keys(encoded).length === 0) {
727
+ throw new TxPlanError(`${name}.upsert() needs at least one column`);
728
+ }
729
+ if (options.onConflict.length === 0) {
730
+ throw new TxPlanError(`${name}.upsert() needs at least one onConflict column`);
731
+ }
732
+ return this.push(
733
+ { op: "upsert", table: name, values: encoded, onConflict: options.onConflict },
734
+ `${name}.upsert()`
735
+ );
736
+ },
671
737
  insertMany: (rows) => {
672
738
  if (rows.length === 0) {
673
739
  return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
@@ -841,10 +907,11 @@ function translateRejection(err, builder) {
841
907
  function makeTypedTable(name, raw2) {
842
908
  return {
843
909
  insert: (data) => raw2.insert(name, data),
910
+ upsert: (data, opts) => raw2.upsert(name, data, opts),
844
911
  update: (id, data) => raw2.update(name, id, data),
845
912
  delete: (id) => raw2.delete(name, id),
846
913
  findById: (id) => raw2.findById(name, id),
847
- findMany: (query) => raw2.findMany(name, query)
914
+ findMany: (query, opts) => raw2.findMany(name, query, opts)
848
915
  };
849
916
  }
850
917
  function makeTypedDB(schema, raw2) {