@palbase/backend 22.1.0 → 23.0.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/dist/bin/palbase-backend.cjs +439 -18
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -4
- package/dist/{chunk-SQC5EIWY.js → chunk-FSGSB42K.js} +11 -6
- package/dist/chunk-FSGSB42K.js.map +1 -0
- package/dist/{chunk-I3ON7MYF.js → chunk-HAF67F2H.js} +73 -16
- package/dist/chunk-HAF67F2H.js.map +1 -0
- package/dist/{chunk-QMVK4X3V.js → chunk-OMRTHM4X.js} +98 -98
- package/dist/chunk-OMRTHM4X.js.map +1 -0
- package/dist/{chunk-74XDEF5J.js → chunk-REZU6UKT.js} +432 -17
- package/dist/chunk-REZU6UKT.js.map +1 -0
- package/dist/{chunk-YL4C5NRY.js → chunk-Y5HXVUMP.js} +2 -2
- package/dist/{chunk-N32VDWKH.js → chunk-ZC6Q2BRD.js} +4 -59
- package/dist/chunk-ZC6Q2BRD.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +3 -21
- package/dist/db/env.d.ts +3 -21
- package/dist/db/index.cjs +72 -15
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +3 -2
- package/dist/db/index.d.ts +3 -2
- package/dist/db/index.js +1 -1
- package/dist/{endpoint-BVT6jcVW.d.cts → endpoint-BavvbW4P.d.ts} +77 -7
- package/dist/{endpoint-BVT6jcVW.d.ts → endpoint-i8TTCohk.d.cts} +77 -7
- package/dist/engine/index.cjs +439 -18
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -5
- package/dist/engine/index.d.ts +6 -5
- package/dist/engine/index.js +3 -3
- package/dist/{index-vwHoS0l2.d.ts → index-B3jmmItD.d.ts} +111 -5
- package/dist/{index-CCZqzych.d.ts → index-B7YBEG5w.d.ts} +96 -71
- package/dist/{index-BqCiHao8.d.cts → index-Bmvx1EvJ.d.cts} +111 -5
- package/dist/{index-BS1gW4nV.d.cts → index-E7OscPJT.d.cts} +96 -71
- package/dist/index.cjs +285 -1099
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -1134
- package/dist/index.d.ts +89 -1134
- package/dist/index.js +107 -902
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +3 -57
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -2
- package/dist/openapi/index.d.ts +3 -2
- package/dist/openapi/index.js +5 -22
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-Bsuf-orT.d.ts → registry-C3H2uPeZ.d.cts} +13 -169
- package/dist/{registry-BWttGlaT.d.cts → registry-DY3d9l1k.d.ts} +13 -169
- package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
- package/dist/stack.cjs.map +1 -0
- package/dist/stack.d.cts +76 -0
- package/dist/stack.d.ts +76 -0
- package/dist/stack.js +1 -0
- package/docs/README.md +7 -6
- package/docs/llms-full.txt +7 -260
- package/docs/llms.txt +0 -2
- package/package.json +6 -6
- package/template/package.json +1 -1
- package/dist/chunk-74XDEF5J.js.map +0 -1
- package/dist/chunk-I3ON7MYF.js.map +0 -1
- package/dist/chunk-N32VDWKH.js.map +0 -1
- package/dist/chunk-QMVK4X3V.js.map +0 -1
- package/dist/chunk-SQC5EIWY.js.map +0 -1
- package/dist/purchases/keys.cjs.map +0 -1
- package/dist/purchases/keys.d.cts +0 -42
- package/dist/purchases/keys.d.ts +0 -42
- package/dist/purchases/keys.js +0 -1
- package/docs/config.md +0 -147
- package/docs/resources.md +0 -97
- package/template/config/secrets.ts +0 -24
- /package/dist/{chunk-YL4C5NRY.js.map → chunk-Y5HXVUMP.js.map} +0 -0
- /package/dist/{purchases/keys.js.map → stack.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 { 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}\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\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":";AAkGO,IAAM,SAAwB,uBAAO,IAAI,wBAAwB;AAMxE,IAAM,eAA8B,uBAAO,IAAI,6BAA6B;AAS5E,IAAM,gBAA+B,uBAAO,IAAI,8BAA8B;AAS9E,IAAM,gBAA+B,uBAAO,IAAI,8BAA8B;AAgB9E,SAAS,UAAU,QAAiC;AAIlD,QAAM,OACJ,OAAO,WAAW,aACb,SACE,OAAqC,eACtC;AACR,SAAO;AACT;AAIA,SAAS,UAAU,SAAuC;AACxD,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,SAAS,MAAM,GAAG;AAC1D,YAAQ,MAAM,IAAI,CAAC;AAAA,EACrB;AACA,SAAO,QAAQ,MAAM;AACvB;AAGA,SAAS,eAAe,SAAuD;AAC7E,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,SAAS,YAAY,GAAG;AAChE,YAAQ,YAAY,IAAI,CAAC;AAAA,EAC3B;AACA,SAAO,QAAQ,YAAY;AAC7B;AAKO,SAAS,YACd,QACA,QACA,QACA,SACA,SACM;AACN,QAAM,UAAU,UAAU,MAAM;AAChC,QAAM,SAAS,UAAU,OAAO;AAChC,QAAM,SAAS,eAAe,OAAO;AACrC,QAAM,UAAU,OAAO,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC9E,QAAM,QAAmB,EAAE,QAAQ,SAAS,QAAQ,SAAS,OAAO;AAIpE,QAAM,eAAe,QAAQ,aAAa;AAC1C,MAAI,gBAAgB,aAAa,MAAM,MAAM,QAAW;AACtD,UAAM,eAAe,aAAa,MAAM;AAAA,EAC1C;AAGA,QAAM,eAAe,QAAQ,aAAa;AAC1C,MAAI,gBAAgB,aAAa,MAAM,MAAM,QAAW;AACtD,UAAM,SAAS,aAAa,MAAM;AAAA,EACpC;AACA,SAAO,KAAK,KAAK;AACnB;AAOO,SAAS,YAAY,QAAgB,QAAgB,MAAuB;AACjF,QAAM,UAAU,UAAU,MAAM;AAChC,QAAM,SAAS,eAAe,OAAO;AACrC,GAAC,OAAO,MAAM,MAAM,CAAC,GAAG,KAAK,IAAI;AAGjC,QAAM,SAAS,QAAQ,MAAM;AAC7B,MAAI,QAAQ;AACV,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM;AACpD,QAAI,OAAO;AACT,YAAM,OAAO,KAAK,IAAI;AACtB,YAAM,OAAO,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAAA,IAC/C;AAAA,EACF;AACF;AA0BO,SAAS,aAAa,QAAgB,QAAgB,QAAiC;AAC5F,QAAM,UAAU,UAAU,MAAM;AAChC,QAAM,SAAS,QAAQ,MAAM;AAC7B,QAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM;AACrD,MAAI,OAAO;AACT,UAAM,SAAS;AACf;AAAA,EACF;AACA,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,SAAS,aAAa,GAAG;AACjE,YAAQ,aAAa,IAAI,CAAC;AAAA,EAC5B;AACA,QAAM,eAAe,QAAQ,aAAa;AAC1C,MAAI,aAAc,cAAa,MAAM,IAAI;AAC3C;AAOO,SAAS,UAAU,MAA2B;AACnD,QAAM,UAAU,UAAU,IAAI;AAC9B,QAAM,SAAS,QAAQ,MAAM,KAAK,CAAC;AACnC,QAAM,eAAe,QAAQ,aAAa;AAC1C,MAAI,cAAc;AAChB,eAAW,SAAS,QAAQ;AAC1B,YAAM,WAAW,aAAa,MAAM,MAAM;AAC1C,UAAI,YAAY,MAAM,iBAAiB,QAAW;AAChD,cAAM,eAAe;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACA,QAAM,eAAe,QAAQ,aAAa;AAC1C,MAAI,cAAc;AAChB,eAAW,SAAS,QAAQ;AAC1B,YAAM,WAAW,aAAa,MAAM,MAAM;AAC1C,UAAI,YAAY,MAAM,WAAW,QAAW;AAC1C,cAAM,SAAS;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO,IAAI,CAAC,OAAO;AAAA,IACxB,GAAG;AAAA,IACH,QAAQ,EAAE,OAAO,MAAM;AAAA,IACvB,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,MAAM,EAAE,IAAI,CAAC;AAAA,EAC/D,EAAE;AACJ;;;AClQO,IAAM,mBAAkC,uBAAO,IAAI,2BAA2B;AAU9E,SAAS,YAAY,KAAgC;AAC1D,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO;AACpD,QAAM,IAAI;AACV,SACE,EAAE,gBAAgB,MAAM,QACxB,OAAO,EAAE,WAAW,YACpB,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,qBAAqB;AAElC;AAEO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEhB,CAAiB,gBAAgB,IAAI;AAAA,EAErC,YAAY,QAAgB,OAAe,kBAA0B,MAAgB;AACnF,UAAM,gBAAgB;AACtB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,mBAAmB;AACxB,QAAI,SAAS,QAAW;AACtB,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,WAML;AACA,UAAM,SAMF;AAAA,MACF,OAAO,KAAK;AAAA,MACZ,mBAAmB,KAAK;AAAA,MACxB,QAAQ,KAAK;AAAA,IACf;AACA,QAAI,WAAW;AACb,aAAO,aAAa;AAAA,IACtB;AACA,QAAI,KAAK,SAAS,QAAW;AAC3B,aAAO,OAAO,KAAK;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACF;AASO,IAAM,WAAN,cAAuB,UAAU;AAAA,EACtC,YAAY,QAAgB,MAAc,aAAqB,MAAgB;AAC7E,UAAM,QAAQ,MAAM,aAAa,IAAI;AACrC,SAAK,OAAO;AAAA,EACd;AACF;AAKA,IAAe,iBAAf,cAAsC,UAAU;AAAA,EACpC,YACR,QACA,aACA,MACA,SACA,MACA,MACA;AACA,UAAM,QAAQ,QAAQ,aAAa,WAAW,eAAe,IAAI,GAAG,IAAI;AACxE,SAAK,OAAO;AAAA,EACd;AACF;AAIA,SAAS,eAAe,MAAsB;AAC5C,QAAM,SAAS,KAAK,QAAQ,sBAAsB,OAAO;AACzD,SAAO,OAAO,OAAO,CAAC,EAAE,YAAY,IAAI,OAAO,MAAM,CAAC,EAAE,YAAY;AACtE;AAQO,IAAM,aAAN,cAAyB,eAAe;AAAA,EAE7C,YAAY,MAAsB,SAAkB;AAClD,UAAM,KAAK,eAAe,cAAc,SAAS,QAAW,IAAI;AAAA,EAClE;AACF;AAGO,IAAM,eAAN,cAA2B,eAAe;AAAA,EAC/C,YAAY,SAAkB,MAAe,MAAgB;AAC3D,UAAM,KAAK,gBAAgB,gBAAgB,SAAS,MAAM,IAAI;AAAA,EAChE;AACF;AAGO,IAAM,YAAN,cAAwB,eAAe;AAAA,EAC5C,YAAY,SAAkB,MAAe,MAAgB;AAC3D,UAAM,KAAK,aAAa,aAAa,SAAS,MAAM,IAAI;AAAA,EAC1D;AACF;AAGO,IAAM,WAAN,cAAuB,eAAe;AAAA,EAC3C,YAAY,SAAkB,MAAe,MAAgB;AAC3D,UAAM,KAAK,aAAa,YAAY,SAAS,MAAM,IAAI;AAAA,EACzD;AACF;AAGO,IAAM,WAAN,cAAuB,eAAe;AAAA,EAC3C,YAAY,SAAkB,MAAe,MAAgB;AAC3D,UAAM,KAAK,YAAY,YAAY,SAAS,MAAM,IAAI;AAAA,EACxD;AACF;AA4BO,IAAM,kBAAN,cAA8B,eAAe;AAAA,EAElD,YAAY,MAA2B,SAAkB;AACvD,UAAM,KAAK,qBAAqB,mBAAmB,SAAS,QAAW,IAAI;AAAA,EAC7E;AACF;","names":[]}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__requestALS,
|
|
3
3
|
__runWithRuntime
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FSGSB42K.js";
|
|
5
5
|
import {
|
|
6
6
|
getRoutes,
|
|
7
7
|
isHttpError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OMRTHM4X.js";
|
|
9
9
|
|
|
10
10
|
// src/engine/config.ts
|
|
11
11
|
var BootRefused = class extends Error {
|
|
@@ -441,7 +441,42 @@ function searchConfigFor(table) {
|
|
|
441
441
|
);
|
|
442
442
|
}
|
|
443
443
|
const search = t.search;
|
|
444
|
-
const
|
|
444
|
+
const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
|
|
445
|
+
const validity = search?.validity === true ? { validity: true } : {};
|
|
446
|
+
if (search?.from !== void 0 && search.model !== void 0) {
|
|
447
|
+
const m = search.model;
|
|
448
|
+
const embed = {
|
|
449
|
+
model: m.model,
|
|
450
|
+
apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
|
|
451
|
+
...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
|
|
452
|
+
...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
|
|
453
|
+
};
|
|
454
|
+
const metric = search.metric ?? "cosine";
|
|
455
|
+
const ftsOn = search.text !== false;
|
|
456
|
+
const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
|
|
457
|
+
if (vectorCols.length > 0) {
|
|
458
|
+
return {
|
|
459
|
+
pk,
|
|
460
|
+
cols,
|
|
461
|
+
colSet: new Set(cols),
|
|
462
|
+
ftsCols: ftsColsNew,
|
|
463
|
+
legs: [{ column: vectorCols[0], metric, embed }],
|
|
464
|
+
...synonyms,
|
|
465
|
+
...validity
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
pk,
|
|
470
|
+
cols,
|
|
471
|
+
colSet: new Set(cols),
|
|
472
|
+
ftsCols: ftsColsNew,
|
|
473
|
+
legs: [],
|
|
474
|
+
chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
|
|
475
|
+
...synonyms,
|
|
476
|
+
...validity
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
|
|
445
480
|
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
|
|
446
481
|
let legs;
|
|
447
482
|
if (rawLegs.length > 0) {
|
|
@@ -467,7 +502,7 @@ function searchConfigFor(table) {
|
|
|
467
502
|
legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
|
|
468
503
|
}
|
|
469
504
|
if (ftsCols.length === 0 && legs.length === 0) return null;
|
|
470
|
-
return { pk, cols, colSet: new Set(cols), ftsCols, legs };
|
|
505
|
+
return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
|
|
471
506
|
}
|
|
472
507
|
function pickLeg(table, legs, using) {
|
|
473
508
|
if (legs.length === 0) return null;
|
|
@@ -483,7 +518,74 @@ function pickLeg(table, legs, using) {
|
|
|
483
518
|
if (legs.length === 1) return legs[0];
|
|
484
519
|
throw new Error(`search(${table}): birden \xE7ok vekt\xF6r kolu var \u2014 'using' ile se\xE7in (FR-013) \u2014 salt metin ar\u0131yorsan mode:"text" kullan`);
|
|
485
520
|
}
|
|
521
|
+
var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
522
|
+
function parseHalfLife(s) {
|
|
523
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(s);
|
|
524
|
+
const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
|
|
525
|
+
if (!Number.isFinite(sec) || sec <= 0) {
|
|
526
|
+
throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
|
|
527
|
+
}
|
|
528
|
+
return sec;
|
|
529
|
+
}
|
|
486
530
|
var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
|
|
531
|
+
function ftsExprOf(ftsCols) {
|
|
532
|
+
return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
|
|
533
|
+
}
|
|
534
|
+
function expandSynonyms(query, map) {
|
|
535
|
+
const lower = {};
|
|
536
|
+
for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
|
|
537
|
+
return query.split(/("[^"]*")/).map((seg) => {
|
|
538
|
+
if (seg.startsWith('"')) return seg;
|
|
539
|
+
return seg.split(/(\s+)/).map((tok) => {
|
|
540
|
+
if (tok === "" || /^\s+$/.test(tok)) return tok;
|
|
541
|
+
const alts = lower[tok.toLowerCase()];
|
|
542
|
+
return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
|
|
543
|
+
}).join("");
|
|
544
|
+
}).join("");
|
|
545
|
+
}
|
|
546
|
+
var searchLogger = null;
|
|
547
|
+
function logNoResults(table, query, mode, n) {
|
|
548
|
+
if (n === 0) {
|
|
549
|
+
searchLogger?.("palbase.search.no_results", {
|
|
550
|
+
table,
|
|
551
|
+
query: (query ?? "").slice(0, 200),
|
|
552
|
+
mode: mode ?? "hybrid"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
|
|
557
|
+
const bind = [];
|
|
558
|
+
const add = (v) => {
|
|
559
|
+
bind.push(v);
|
|
560
|
+
return `$${bind.length}`;
|
|
561
|
+
};
|
|
562
|
+
const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
|
|
563
|
+
const parts = facets.map(
|
|
564
|
+
(col) => `(SELECT '${col.replace(/'/g, "''")}' AS f, t.${quoteIdent(col)}::text AS v, count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql} GROUP BY 2 ORDER BY 3 DESC LIMIT 20)`
|
|
565
|
+
);
|
|
566
|
+
const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const col of facets) out[col] = [];
|
|
569
|
+
for (const r of rows) {
|
|
570
|
+
if (typeof r.f === "string" && out[r.f] !== void 0) {
|
|
571
|
+
out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
function validitySql(v, add) {
|
|
577
|
+
if (v === "all") return "";
|
|
578
|
+
if (v !== void 0) {
|
|
579
|
+
const p = add(v.asOf);
|
|
580
|
+
return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
|
|
581
|
+
}
|
|
582
|
+
return ` AND t."valid_to" IS NULL`;
|
|
583
|
+
}
|
|
584
|
+
function excludeSql(pk, ids, add) {
|
|
585
|
+
if (ids.length === 0) return "";
|
|
586
|
+
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
587
|
+
return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
588
|
+
}
|
|
487
589
|
function compileWhere(table, colSet, where, add) {
|
|
488
590
|
const parts = [];
|
|
489
591
|
for (const [col, cond] of Object.entries(where)) {
|
|
@@ -513,6 +615,15 @@ function compileWhere(table, colSet, where, add) {
|
|
|
513
615
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
514
616
|
}
|
|
515
617
|
var cachedVectorSchema = null;
|
|
618
|
+
var cachedTrgmSchema = null;
|
|
619
|
+
async function trgmSchemaOf(live) {
|
|
620
|
+
if (cachedTrgmSchema !== null) return cachedTrgmSchema;
|
|
621
|
+
const rows = await live.unsafe(
|
|
622
|
+
"select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
|
|
623
|
+
);
|
|
624
|
+
cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
|
|
625
|
+
return cachedTrgmSchema;
|
|
626
|
+
}
|
|
516
627
|
async function vectorSchemaWithGuc(runner) {
|
|
517
628
|
if (cachedVectorSchema !== null) {
|
|
518
629
|
await runner.unsafe(
|
|
@@ -628,12 +739,128 @@ function createOps(tx) {
|
|
|
628
739
|
if (!cfg) {
|
|
629
740
|
throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
630
741
|
}
|
|
742
|
+
const excl = params.__excludeIds ?? [];
|
|
743
|
+
if (params.facets !== void 0) {
|
|
744
|
+
for (const col of params.facets) {
|
|
745
|
+
if (!cfg.colSet.has(col)) {
|
|
746
|
+
throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (params.validity !== void 0 && cfg.validity !== true) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
|
|
631
756
|
const rawLimit = params.limit ?? 20;
|
|
632
757
|
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
633
758
|
throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
|
|
634
759
|
}
|
|
635
760
|
const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
|
|
636
761
|
const pool = Math.max(limit * 3, 30);
|
|
762
|
+
if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
|
|
763
|
+
throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
|
|
764
|
+
}
|
|
765
|
+
let recencyMul = "";
|
|
766
|
+
if (params.recency !== void 0) {
|
|
767
|
+
const { field, halfLife } = params.recency;
|
|
768
|
+
if (!cfg.colSet.has(field)) {
|
|
769
|
+
throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
|
|
770
|
+
}
|
|
771
|
+
recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
|
|
772
|
+
}
|
|
773
|
+
let boostMul = "";
|
|
774
|
+
if (params.boost !== void 0) {
|
|
775
|
+
const { field, weight } = params.boost;
|
|
776
|
+
if (!cfg.colSet.has(field)) {
|
|
777
|
+
throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
|
|
778
|
+
}
|
|
779
|
+
if (typeof weight !== "number" || !Number.isFinite(weight)) {
|
|
780
|
+
throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
|
|
781
|
+
}
|
|
782
|
+
const g = `greatest(t.${quoteIdent(field)},0)`;
|
|
783
|
+
boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
|
|
784
|
+
}
|
|
785
|
+
const scoreMul = boostMul + recencyMul;
|
|
786
|
+
if (cfg.chunk !== void 0) {
|
|
787
|
+
const ck = cfg.chunk;
|
|
788
|
+
const pidQ = quoteIdent(`parent_${cfg.pk}`);
|
|
789
|
+
const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
|
|
790
|
+
const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
|
|
791
|
+
let qvC = Array.isArray(params.vector) ? params.vector : null;
|
|
792
|
+
if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
|
|
793
|
+
try {
|
|
794
|
+
qvC = await embedQuery(ck.embed, params.query);
|
|
795
|
+
} catch (e) {
|
|
796
|
+
if (!wantTextC) throw e;
|
|
797
|
+
qvC = null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (!wantTextC && qvC === null) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const bindC = [];
|
|
806
|
+
const addC = (v) => {
|
|
807
|
+
bindC.push(v);
|
|
808
|
+
return `$${bindC.length}`;
|
|
809
|
+
};
|
|
810
|
+
const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
811
|
+
const liveC = await at();
|
|
812
|
+
const K2 = 50;
|
|
813
|
+
const cpool = Math.max(limit * 9, 90);
|
|
814
|
+
let semC = "";
|
|
815
|
+
let kwC = "";
|
|
816
|
+
if (qvC !== null) {
|
|
817
|
+
const sch = await vectorSchemaWithGuc(liveC);
|
|
818
|
+
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
819
|
+
const vp = addC(toVectorLiteral(qvC));
|
|
820
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c WHERE c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
821
|
+
}
|
|
822
|
+
let qpC = "";
|
|
823
|
+
let qpCIdx = -1;
|
|
824
|
+
if (wantTextC) {
|
|
825
|
+
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
826
|
+
qpC = addC(qTextC);
|
|
827
|
+
qpCIdx = bindC.length - 1;
|
|
828
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
829
|
+
}
|
|
830
|
+
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
831
|
+
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
832
|
+
const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
|
|
833
|
+
const assembleC = (kwIn, withKwn) => {
|
|
834
|
+
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
835
|
+
let fusedSrc;
|
|
836
|
+
if (semC !== "" && kwIn !== "") {
|
|
837
|
+
fusedSrc = `WITH sem AS (${semC}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.pid, kw.pid) AS pid, COALESCE(sem.chunk_seq, kw.chunk_seq) AS chunk_seq, (COALESCE(1.0/(${K2} + sem.r), 0) + COALESCE(1.0/(${K2} + kw.r), 0))::float8 AS cscore FROM sem FULL OUTER JOIN kw ON sem.pid = kw.pid AND sem.chunk_seq = kw.chunk_seq)`;
|
|
838
|
+
} else if (semC !== "") {
|
|
839
|
+
fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
|
|
840
|
+
} else {
|
|
841
|
+
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
842
|
+
}
|
|
843
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid WHERE true${whereC}`;
|
|
844
|
+
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
845
|
+
};
|
|
846
|
+
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
847
|
+
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
848
|
+
const tsch = await trgmSchemaOf(liveC);
|
|
849
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE ${qpC} OPERATOR(${quoteIdent(tsch)}.<%) coalesce(c.content,'') ORDER BY r LIMIT ${cpool}`;
|
|
850
|
+
const retryBindC = bindC.slice();
|
|
851
|
+
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
852
|
+
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
853
|
+
}
|
|
854
|
+
for (const r of rowsC) delete r._kwn;
|
|
855
|
+
const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
|
|
856
|
+
if (params.facets !== void 0 && params.facets.length > 0) {
|
|
857
|
+
Object.assign(outC, {
|
|
858
|
+
_facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
logNoResults(table, params.query, params.mode, outC.length);
|
|
862
|
+
return outC;
|
|
863
|
+
}
|
|
637
864
|
const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
|
|
638
865
|
const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
|
|
639
866
|
const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
|
|
@@ -658,7 +885,8 @@ function createOps(tx) {
|
|
|
658
885
|
bind.push(v);
|
|
659
886
|
return `$${bind.length}`;
|
|
660
887
|
};
|
|
661
|
-
const
|
|
888
|
+
const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
|
|
889
|
+
const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
|
|
662
890
|
const live = await at();
|
|
663
891
|
const K = 50;
|
|
664
892
|
let semSql = "";
|
|
@@ -667,7 +895,7 @@ function createOps(tx) {
|
|
|
667
895
|
const sch = await vectorSchemaWithGuc(live);
|
|
668
896
|
const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
|
|
669
897
|
let exactOrder = "";
|
|
670
|
-
if (
|
|
898
|
+
if (userWhere !== "") {
|
|
671
899
|
const probeRows = await live.unsafe(
|
|
672
900
|
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
673
901
|
bind.slice()
|
|
@@ -680,21 +908,208 @@ function createOps(tx) {
|
|
|
680
908
|
const vp = add(toVectorLiteral(qv));
|
|
681
909
|
semSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY (t.${quoteIdent(leg.column)} OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrder}) AS r FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
682
910
|
}
|
|
911
|
+
let qpRef = "";
|
|
912
|
+
let qpIdx = -1;
|
|
683
913
|
if (wantText) {
|
|
684
|
-
const
|
|
914
|
+
const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
915
|
+
const qp = add(qText);
|
|
916
|
+
qpIdx = bind.length - 1;
|
|
917
|
+
qpRef = qp;
|
|
685
918
|
kwSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(t.palbase_fts, websearch_to_tsquery('simple', ${qp})) DESC) AS r FROM ${quoteIdent(table)} t WHERE t.palbase_fts @@ websearch_to_tsquery('simple', ${qp})${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
686
919
|
}
|
|
687
920
|
const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
921
|
+
const msP = params.minScore === void 0 ? "" : add(params.minScore);
|
|
922
|
+
const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
|
|
923
|
+
const assemble = (kwIn, withKwn) => {
|
|
924
|
+
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
925
|
+
let inner;
|
|
926
|
+
let orderScore;
|
|
927
|
+
if (semSql !== "" && kwIn !== "") {
|
|
928
|
+
const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
|
|
929
|
+
inner = `WITH sem AS (${semSql}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.id, kw.id) AS id, (COALESCE(1.0/(${K} + sem.r), 0) + COALESCE(1.0/(${K} + kw.r), 0))::float8 AS _score FROM sem FULL OUTER JOIN kw ON sem.id = kw.id) SELECT ${colList}, ${score} AS _score${hlCol}${kwnCol} FROM fused JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id`;
|
|
930
|
+
orderScore = scoreMul === "" ? "fused._score" : "_score";
|
|
931
|
+
} else {
|
|
932
|
+
const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
|
|
933
|
+
const alias = semSql !== "" ? "sem" : "kw";
|
|
934
|
+
const base = `(1.0/(${K} + ${alias}.r))::float8`;
|
|
935
|
+
const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
|
|
936
|
+
inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
|
|
937
|
+
orderScore = "_score";
|
|
938
|
+
}
|
|
939
|
+
return msP === "" ? `${inner} ORDER BY ${orderScore} DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msP} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
940
|
+
};
|
|
941
|
+
let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
|
|
942
|
+
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
943
|
+
const tsch = await trgmSchemaOf(live);
|
|
944
|
+
const expr = ftsExprOf(cfg.ftsCols);
|
|
945
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${qpRef} OPERATOR(${quoteIdent(tsch)}.<%) ${expr}${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
946
|
+
const retryBind = bind.slice();
|
|
947
|
+
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
948
|
+
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
949
|
+
}
|
|
950
|
+
for (const r of rows) delete r._kwn;
|
|
951
|
+
const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
|
|
952
|
+
if (params.facets !== void 0 && params.facets.length > 0) {
|
|
953
|
+
Object.assign(out, {
|
|
954
|
+
_facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
logNoResults(table, params.query, params.mode, out.length);
|
|
958
|
+
return out;
|
|
959
|
+
},
|
|
960
|
+
/**
|
|
961
|
+
* similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
|
|
962
|
+
* okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
|
|
963
|
+
* şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
|
|
964
|
+
* kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
|
|
965
|
+
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
966
|
+
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
967
|
+
*/
|
|
968
|
+
async similar(table, id, opts = {}) {
|
|
969
|
+
const cfg = searchConfigFor(table);
|
|
970
|
+
if (!cfg) {
|
|
971
|
+
throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
972
|
+
}
|
|
973
|
+
const live = await at();
|
|
974
|
+
let v;
|
|
975
|
+
if (cfg.chunk !== void 0) {
|
|
976
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
977
|
+
const rows = await live.unsafe(
|
|
978
|
+
`SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} = $1 AND c.embedding IS NOT NULL`,
|
|
979
|
+
[id]
|
|
980
|
+
);
|
|
981
|
+
v = rows?.[0]?.v;
|
|
691
982
|
} else {
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
983
|
+
const leg = pickLeg(table, cfg.legs, opts.using);
|
|
984
|
+
if (leg === null) {
|
|
985
|
+
throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
|
|
986
|
+
}
|
|
987
|
+
const rows = await live.unsafe(
|
|
988
|
+
`SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
|
|
989
|
+
[id]
|
|
990
|
+
);
|
|
991
|
+
v = rows?.[0]?.v;
|
|
992
|
+
}
|
|
993
|
+
if (typeof v !== "string") {
|
|
994
|
+
throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
|
|
995
|
+
}
|
|
996
|
+
const p = {
|
|
997
|
+
vector: JSON.parse(v),
|
|
998
|
+
mode: "vector",
|
|
999
|
+
where: opts.where,
|
|
1000
|
+
limit: opts.limit,
|
|
1001
|
+
using: opts.using,
|
|
1002
|
+
minScore: opts.minScore,
|
|
1003
|
+
recency: opts.recency,
|
|
1004
|
+
blocksPerRow: opts.blocksPerRow,
|
|
1005
|
+
validity: opts.validity,
|
|
1006
|
+
boost: opts.boost,
|
|
1007
|
+
__excludeIds: [id]
|
|
1008
|
+
};
|
|
1009
|
+
return ops.search(table, p);
|
|
1010
|
+
},
|
|
1011
|
+
/**
|
|
1012
|
+
* recommend (T018, FR-023): positive/negative id kümelerinden öneri.
|
|
1013
|
+
* Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
|
|
1014
|
+
* negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
|
|
1015
|
+
* ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
|
|
1016
|
+
* Vektör matematiği istemciye inmez; bulunamayan positive/negative
|
|
1017
|
+
* adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
|
|
1018
|
+
*/
|
|
1019
|
+
async recommend(table, opts) {
|
|
1020
|
+
const positive = opts?.positive;
|
|
1021
|
+
if (!Array.isArray(positive) || positive.length === 0) {
|
|
1022
|
+
throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
|
|
1023
|
+
}
|
|
1024
|
+
const negative = Array.isArray(opts.negative) ? opts.negative : [];
|
|
1025
|
+
const cfg = searchConfigFor(table);
|
|
1026
|
+
if (!cfg) {
|
|
1027
|
+
throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
695
1028
|
}
|
|
696
|
-
|
|
697
|
-
|
|
1029
|
+
let leg = null;
|
|
1030
|
+
if (cfg.chunk === void 0) {
|
|
1031
|
+
leg = pickLeg(table, cfg.legs, opts.using);
|
|
1032
|
+
if (leg === null) {
|
|
1033
|
+
throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const live = await at();
|
|
1037
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
1038
|
+
const bind = [];
|
|
1039
|
+
const add = (x) => {
|
|
1040
|
+
bind.push(x);
|
|
1041
|
+
return `$${bind.length}`;
|
|
1042
|
+
};
|
|
1043
|
+
const avgSel = (ids) => {
|
|
1044
|
+
const inList = ids.map((x) => add(x)).join(", ");
|
|
1045
|
+
return cfg.chunk !== void 0 ? `SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} IN (${inList}) AND c.embedding IS NOT NULL` : `SELECT ${quoteIdent(sch)}.avg(t.${quoteIdent(leg.column)}) AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} IN (${inList}) AND t.${quoteIdent(leg.column)} IS NOT NULL`;
|
|
1046
|
+
};
|
|
1047
|
+
const targetSql = negative.length === 0 ? `WITH pos AS (${avgSel(positive)}) SELECT pos.v AS v, (pos.v IS NULL) AS pos_missing FROM pos` : `WITH pos AS (${avgSel(positive)}), neg AS (${avgSel(negative)}) SELECT (pos.v OPERATOR(${quoteIdent(sch)}.+) (pos.v OPERATOR(${quoteIdent(sch)}.-) neg.v)) AS v, (pos.v IS NULL) AS pos_missing, (neg.v IS NULL) AS neg_missing FROM pos, neg`;
|
|
1048
|
+
const rows = await live.unsafe(targetSql, bind);
|
|
1049
|
+
const r0 = rows?.[0];
|
|
1050
|
+
if (r0 === void 0 || r0.pos_missing === true) {
|
|
1051
|
+
throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
|
|
1052
|
+
}
|
|
1053
|
+
if (negative.length > 0 && r0.neg_missing === true) {
|
|
1054
|
+
throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof r0.v !== "string") {
|
|
1057
|
+
throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
|
|
1058
|
+
}
|
|
1059
|
+
const p = {
|
|
1060
|
+
vector: JSON.parse(r0.v),
|
|
1061
|
+
mode: "vector",
|
|
1062
|
+
where: opts.where,
|
|
1063
|
+
limit: opts.limit,
|
|
1064
|
+
using: opts.using,
|
|
1065
|
+
minScore: opts.minScore,
|
|
1066
|
+
recency: opts.recency,
|
|
1067
|
+
blocksPerRow: opts.blocksPerRow,
|
|
1068
|
+
validity: opts.validity,
|
|
1069
|
+
boost: opts.boost,
|
|
1070
|
+
__excludeIds: [...positive, ...negative]
|
|
1071
|
+
};
|
|
1072
|
+
return ops.search(table, p);
|
|
1073
|
+
},
|
|
1074
|
+
/**
|
|
1075
|
+
* supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
|
|
1076
|
+
* versiyonunu TEK savepoint'te yazar — eski satır kapatılır
|
|
1077
|
+
* (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
|
|
1078
|
+
* yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
|
|
1079
|
+
* randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
|
|
1080
|
+
* kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
|
|
1081
|
+
* ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
|
|
1082
|
+
* kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
|
|
1083
|
+
*/
|
|
1084
|
+
async supersede(table, id, row) {
|
|
1085
|
+
const cfg = searchConfigFor(table);
|
|
1086
|
+
if (cfg === null || cfg.validity !== true) {
|
|
1087
|
+
throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
|
|
1088
|
+
}
|
|
1089
|
+
const live = await at();
|
|
1090
|
+
return live.savepoint(async (sp) => {
|
|
1091
|
+
const newId = row[cfg.pk] ?? crypto.randomUUID();
|
|
1092
|
+
const closed = await sp.unsafe(
|
|
1093
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
|
|
1094
|
+
[newId, id]
|
|
1095
|
+
);
|
|
1096
|
+
if (closed.length === 0) {
|
|
1097
|
+
throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
|
|
1098
|
+
}
|
|
1099
|
+
const data = { ...row, [cfg.pk]: newId };
|
|
1100
|
+
const cols = Object.keys(data);
|
|
1101
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1102
|
+
const rows = await sp.unsafe(
|
|
1103
|
+
`INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
|
|
1104
|
+
asBindParams(table, cols, data)
|
|
1105
|
+
);
|
|
1106
|
+
if (!rows[0]) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
return asTableRow(table, rows[0]);
|
|
1112
|
+
});
|
|
698
1113
|
},
|
|
699
1114
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
700
1115
|
async transaction(cb) {
|
|
@@ -738,6 +1153,7 @@ function createOps(tx) {
|
|
|
738
1153
|
var currentSchema = {};
|
|
739
1154
|
function setSchema(schema) {
|
|
740
1155
|
cachedVectorSchema = null;
|
|
1156
|
+
cachedTrgmSchema = null;
|
|
741
1157
|
const s = schema;
|
|
742
1158
|
currentSchema = (s && "default" in s ? s.default : s) ?? {};
|
|
743
1159
|
}
|
|
@@ -1170,7 +1586,6 @@ async function createApp(opts) {
|
|
|
1170
1586
|
Notifications: modules.Notifications ?? stubModule("Notifications"),
|
|
1171
1587
|
Flags: modules.Flags ?? stubModule("Flags"),
|
|
1172
1588
|
Realtime: modules.Realtime ?? stubModule("Realtime"),
|
|
1173
|
-
Purchases: modules.Purchases ?? stubModule("Purchases"),
|
|
1174
1589
|
// Named, never undefined. A backend started without a secrets client
|
|
1175
1590
|
// that returned `undefined` here would fail inside the handler as
|
|
1176
1591
|
// "Cannot read properties of undefined", which says nothing about what
|
|
@@ -1471,4 +1886,4 @@ export {
|
|
|
1471
1886
|
installEgressFence,
|
|
1472
1887
|
createApp
|
|
1473
1888
|
};
|
|
1474
|
-
//# sourceMappingURL=chunk-
|
|
1889
|
+
//# sourceMappingURL=chunk-REZU6UKT.js.map
|