@_linked/server 3.2.0 → 3.3.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/CHANGELOG.md +6 -0
- package/lib/esm/shapes/LinkedServer.js +38 -24
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.js +7 -0
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/fatalError.js +2 -0
- package/lib/esm/utils/fatalError.js.map +1 -1
- package/lib/esm/utils/queryPlane.js +11 -3
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/rawQueries.js +14 -6
- package/lib/esm/utils/rawQueries.js.map +1 -1
- package/lib/esm/utils/rpcExposure.js +13 -5
- package/lib/esm/utils/rpcExposure.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +12 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { packageName } from '../../package.js';
|
|
2
|
+
import { brandLinkedError } from '@_linked/core/utils/linkedError';
|
|
2
3
|
import { Server } from '@_linked/server-utils/utils/Server';
|
|
3
4
|
/**
|
|
4
5
|
* A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP
|
|
@@ -23,6 +24,12 @@ export class RemoteDatasetUnreachableError extends RemoteDatasetError {
|
|
|
23
24
|
/** The data server answered 401 or 403: it refused this dataset's token. */
|
|
24
25
|
export class RemoteDatasetUnauthorizedError extends RemoteDatasetError {
|
|
25
26
|
}
|
|
27
|
+
// Branded, so `isLinkedError(err, RemoteDatasetUnreachableError)` from `@_linked/core` matches an
|
|
28
|
+
// error thrown by another copy of this module, where `instanceof` does not. See
|
|
29
|
+
// https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.
|
|
30
|
+
brandLinkedError(RemoteDatasetError, '@_linked/server', 'RemoteDatasetError');
|
|
31
|
+
brandLinkedError(RemoteDatasetUnreachableError, '@_linked/server', 'RemoteDatasetUnreachableError');
|
|
32
|
+
brandLinkedError(RemoteDatasetUnauthorizedError, '@_linked/server', 'RemoteDatasetUnauthorizedError');
|
|
26
33
|
/** On `ping`, these statuses mean nothing at that url serves the query wire. */
|
|
27
34
|
const PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);
|
|
28
35
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteDataset.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/RemoteDataset.ts"],"names":[],"mappings":"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","sourcesContent":["import { packageName } from '../../package.js';\nimport { Server } from '@_linked/server-utils/utils/Server';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\nimport type { SelectQuery } from '@_linked/core/queries/SelectQuery';\nimport type { CountQuery } from '@_linked/core/queries/CountQuery';\nimport type { AskQuery } from '@_linked/core/queries/AskQuery';\nimport type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';\nimport type { CreateQuery } from '@_linked/core/queries/CreateQuery';\nimport type {\n DeleteQuery,\n DeleteResponse,\n} from '@_linked/core/queries/DeleteQuery';\nimport type {\n SelectResult,\n UpdateResult,\n CreateResult,\n} from '@_linked/core/queries/IntermediateRepresentation';\n\n/**\n * Constructor argument for `new RemoteDataset(config)`: the `config` object of a\n * `linked.{frontend,backend}.datasets.json` entry, passed verbatim.\n */\nexport interface RemoteDatasetConfig {\n /**\n * Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended\n * to it. Without it, queries go to this process's own backend through `Server.call`.\n * Present but empty is an error, never the no-url form.\n */\n url?: string;\n /** Sent as `Authorization: Bearer <token>` when set and `url` is set. */\n token?: string;\n /** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */\n name?: string;\n /** Or pass a fully-qualified URI directly. */\n id?: string;\n}\n\n/**\n * A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP\n * answer came back); `code` is the error body's `code` when the server sent one.\n */\nexport class RemoteDatasetError extends Error {\n /** The data server base url ('' when the configured url is empty). */\n readonly url: string;\n /** The HTTP status; 0 = no HTTP answer. */\n readonly status: number;\n /** The error body's `code`, when the server sent one. */\n readonly code?: string;\n\n constructor(message: string, url: string, status: number, code?: string) {\n super(message);\n this.name = new.target.name;\n this.url = url;\n this.status = status;\n if (code !== undefined) this.code = code;\n }\n}\n\n/**\n * Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an\n * answer that is not the server's. A boot that gets this should stop.\n */\nexport class RemoteDatasetUnreachableError extends RemoteDatasetError {}\n\n/** The data server answered 401 or 403: it refused this dataset's token. */\nexport class RemoteDatasetUnauthorizedError extends RemoteDatasetError {}\n\n/**\n * What the data server's `ping` answered, as is. The generic `@_linked/server` ping answers\n * `{ok: true}`; a gateway may add fields that say which dataset it serves, such as\n * `{ok, projectId, branchName, dataset, forked}`. A body that is not a JSON object reads as `{}`.\n */\nexport type RemoteDatasetPingInfo = Readonly<Record<string, unknown>>;\n\ntype Method =\n | 'ping'\n | 'selectQuery'\n | 'askQuery'\n | 'updateQuery'\n | 'createQuery'\n | 'deleteQuery';\n\n/** On `ping`, these statuses mean nothing at that url serves the query wire. */\nconst PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);\n\n/**\n * An {@link IDataset} whose queries are answered by a linked server.\n *\n * - **Without `url`** the server is this app's own backend: every query goes through\n * `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),\n * and on a backend `Server`'s local shortcut answers it in process. This is the store a\n * browser uses to talk to its backend.\n * - **With `url`** the server is a remote data server: every query posts\n * `{\"args\": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer\n * `token`, through `fetch`, never through `Server` (whose local shortcut would answer the\n * app's own provider) and never retried (a retried write could apply twice). `init()` asks\n * the server's `ping` and throws {@link RemoteDatasetUnreachableError} or\n * {@link RemoteDatasetUnauthorizedError} when it cannot be used.\n *\n * A store is not a Shape: it addresses the backend by package name, so nothing about the\n * store instance crosses the wire. Query routing is decided by the query's shape.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself first; a successful `init()` is memoized, and resolves\n * the ping body (also readable as `info`), so a boot can log which dataset the server serves.\n */\nexport class RemoteDataset implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named; it is not an\n * addressing mechanism, nothing transmits or resolves it.\n */\n readonly id?: string;\n /** The data server base url, trailing slashes trimmed; undefined in the no-url form. */\n readonly url?: string;\n private readonly token?: string;\n private initialized?: Promise<RemoteDatasetPingInfo | undefined>;\n private pingInfo?: RemoteDatasetPingInfo;\n\n constructor(config?: RemoteDatasetConfig | string) {\n if (!config) return;\n if (typeof config === 'string') {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;\n return;\n }\n if (typeof config.url === 'string') {\n this.url = config.url.trim().replace(/\\/+$/, '');\n const token = typeof config.token === 'string' ? config.token.trim() : '';\n if (token) this.token = token;\n }\n if (config.id) {\n this.id = config.id;\n return;\n }\n if (config.name) {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;\n }\n }\n\n /**\n * The body of the last successful `ping` (see {@link init}); undefined before one, and\n * always in the no-url form.\n */\n get info(): RemoteDatasetPingInfo | undefined {\n return this.pingInfo;\n }\n\n /**\n * In the url form, asks the server's `ping` and resolves its body (also kept as\n * {@link info}); resolves undefined in the no-url form.\n */\n init(): Promise<RemoteDatasetPingInfo | undefined> {\n // No initialization needed when queries are routed to the app's own backend.\n if (this.url === undefined) return Promise.resolve(undefined);\n if (!this.initialized) {\n this.initialized = this.post('ping', []).then(\n (body) => {\n const info: RemoteDatasetPingInfo = Object.freeze(\n body && typeof body === 'object' && !Array.isArray(body)\n ? { ...(body as Record<string, unknown>) }\n : {},\n );\n this.pingInfo = info;\n return info;\n },\n (err) => {\n // Not memoized: the next init() asks again.\n this.initialized = undefined;\n throw err;\n },\n );\n }\n return this.initialized;\n }\n\n // Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call\n // as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with\n // `fromJSON()`.\n selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', query.toJSON());\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.call('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.call('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.call('deleteQuery', query.toJSON());\n }\n\n private call<T>(method: Method, json: unknown): Promise<T> {\n if (this.url === undefined) return this.callBackend(method, json);\n return this.post(method, [json]) as Promise<T>;\n }\n\n /**\n * Opts in to `rejectOnError`, so a failed call (HTTP error status, or no\n * provider on the backend) rejects with a `ServerCallError` carrying the\n * status and the server's message instead of reading as an empty result.\n * Whatever a successful call returns, `undefined` included, resolves as is.\n */\n private callBackend<T>(method: string, json: unknown): Promise<T> {\n // The PACKAGE form, not the shape form. It posts to\n // `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,\n // no `instanceNode` — and the backend answers from this package's generic\n // provider without resolving or instantiating any Shape.\n return Server.call(packageName, { method, rejectOnError: true }, json);\n }\n\n /**\n * The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed\n * body (`null` is a valid result: a method that returned `undefined`). Messages name the\n * url and the method, never the token.\n */\n private async post(method: Method, args: unknown[]): Promise<unknown> {\n const base = this.url ?? '';\n if (!base) {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`,\n '',\n 0,\n );\n }\n const target = `${base}/call/${packageName}/${method}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n if (this.token) headers.Authorization = `Bearer ${this.token}`;\n\n let response: Response;\n try {\n response = await globalThis.fetch(target, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args }),\n });\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`,\n base,\n 0,\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n let isJson = false;\n if (text) {\n try {\n body = JSON.parse(text);\n isJson = true;\n } catch {\n body = text;\n }\n }\n\n if (response.ok) {\n if (isJson) return body;\n if (method === 'ping') {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`,\n base,\n response.status,\n );\n }\n if (!text) return undefined;\n throw new RemoteDatasetError(\n `RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`,\n base,\n response.status,\n );\n }\n\n const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : undefined;\n const code = typeof record?.code === 'string' ? record.code : undefined;\n const detail =\n typeof record?.error === 'string'\n ? record.error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${\n code ? ` (${code})` : ''\n }${detail ? `: ${detail}` : ''}`;\n\n if (response.status === 401 || response.status === 403) {\n throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);\n }\n if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {\n throw new RemoteDatasetUnreachableError(message, base, response.status, code);\n }\n throw new RemoteDatasetError(message, base, response.status, code);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RemoteDataset.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/RemoteDataset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAoC5D;;;GAGG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAQ3C,YAAY,OAAe,EAAE,GAAW,EAAE,MAAc,EAAE,IAAa;QACrE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAC3C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,kBAAkB;CAAG;AAExE,4EAA4E;AAC5E,MAAM,OAAO,8BAA+B,SAAQ,kBAAkB;CAAG;AAEzE,kGAAkG;AAClG,gFAAgF;AAChF,iGAAiG;AACjG,gBAAgB,CAAC,kBAAkB,EAAE,iBAAiB,EAAE,oBAAoB,CAAC,CAAC;AAC9E,gBAAgB,CAAC,6BAA6B,EAAE,iBAAiB,EAAE,+BAA+B,CAAC,CAAC;AACpG,gBAAgB,CAAC,8BAA8B,EAAE,iBAAiB,EAAE,gCAAgC,CAAC,CAAC;AAiBtG,gFAAgF;AAChF,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,aAAa;IAYxB,YAAY,MAAqC;QAC/C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,sBAAsB,MAAM,EAAE,CAAC;YACjE,OAAO;QACT,CAAC;QACD,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACjD,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1E,IAAI,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QAChC,CAAC;QACD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,sBAAsB,MAAM,CAAC,IAAI,EAAE,CAAC;QACxE,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,IAAI;QACF,6EAA6E;QAC7E,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAC3C,CAAC,IAAI,EAAE,EAAE;gBACP,MAAM,IAAI,GAA0B,MAAM,CAAC,MAAM,CAC/C,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;oBACtD,CAAC,CAAC,EAAE,GAAI,IAAgC,EAAE;oBAC1C,CAAC,CAAC,EAAE,CACP,CAAC;gBACF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACrB,OAAO,IAAI,CAAC;YACd,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;gBACN,4CAA4C;gBAC5C,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;gBAC7B,MAAM,GAAG,CAAC;YACZ,CAAC,CACF,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,yFAAyF;IACzF,oFAAoF;IACpF,gBAAgB;IAChB,WAAW,CAAC,KAA+B;QACzC,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,QAAQ,CAAC,KAAe;QACtB,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAEO,IAAI,CAAI,MAAc,EAAE,IAAa;QAC3C,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAe,CAAC;IACjD,CAAC;IAED;;;;;OAKG;IACK,WAAW,CAAI,MAAc,EAAE,IAAa;QAClD,oDAAoD;QACpD,+EAA+E;QAC/E,0EAA0E;QAC1E,yDAAyD;QACzD,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;IACzE,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,IAAe;;QAChD,MAAM,IAAI,GAAG,MAAA,IAAI,CAAC,GAAG,mCAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,6BAA6B,CACrC,kFAAkF,MAAM,EAAE,EAC1F,EAAE,EACF,CAAC,CACF,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,IAAI,SAAS,WAAW,IAAI,MAAM,EAAE,CAAC;QACvD,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC;QACF,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;QAE/D,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE;gBACxC,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC/D,MAAM,IAAI,6BAA6B,CACrC,kDAAkD,IAAI,KAAK,MAAM,MAAM,KAAK,EAAE,EAC9E,IAAI,EACJ,CAAC,CACF,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,IAAI,GAAY,SAAS,CAAC;QAC9B,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACxB,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;YAAC,WAAM,CAAC;gBACP,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,IAAI,MAAM;gBAAE,OAAO,IAAI,CAAC;YACxB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,MAAM,IAAI,6BAA6B,CACrC,qCAAqC,IAAI,6CAA6C,EACtF,IAAI,EACJ,QAAQ,CAAC,MAAM,CAChB,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,IAAI;gBAAE,OAAO,SAAS,CAAC;YAC5B,MAAM,IAAI,kBAAkB,CAC1B,qCAAqC,IAAI,aAAa,MAAM,+BAA+B,EAC3F,IAAI,EACJ,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAgC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChG,MAAM,IAAI,GAAG,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAA,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,MAAM,MAAM,GACV,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,CAAA,KAAK,QAAQ;YAC/B,CAAC,CAAC,MAAM,CAAC,KAAK;YACd,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI;gBAChC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC;QAC5B,MAAM,OAAO,GAAG,qCAAqC,IAAI,aAAa,MAAM,SAAS,QAAQ,CAAC,MAAM,GAClG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EACxB,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAEjC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvD,MAAM,IAAI,8BAA8B,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,6BAA6B,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrE,CAAC;CACF","sourcesContent":["import { packageName } from '../../package.js';\nimport { brandLinkedError } from '@_linked/core/utils/linkedError';\nimport { Server } from '@_linked/server-utils/utils/Server';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\nimport type { SelectQuery } from '@_linked/core/queries/SelectQuery';\nimport type { CountQuery } from '@_linked/core/queries/CountQuery';\nimport type { AskQuery } from '@_linked/core/queries/AskQuery';\nimport type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';\nimport type { CreateQuery } from '@_linked/core/queries/CreateQuery';\nimport type {\n DeleteQuery,\n DeleteResponse,\n} from '@_linked/core/queries/DeleteQuery';\nimport type {\n SelectResult,\n UpdateResult,\n CreateResult,\n} from '@_linked/core/queries/IntermediateRepresentation';\n\n/**\n * Constructor argument for `new RemoteDataset(config)`: the `config` object of a\n * `linked.{frontend,backend}.datasets.json` entry, passed verbatim.\n */\nexport interface RemoteDatasetConfig {\n /**\n * Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended\n * to it. Without it, queries go to this process's own backend through `Server.call`.\n * Present but empty is an error, never the no-url form.\n */\n url?: string;\n /** Sent as `Authorization: Bearer <token>` when set and `url` is set. */\n token?: string;\n /** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */\n name?: string;\n /** Or pass a fully-qualified URI directly. */\n id?: string;\n}\n\n/**\n * A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP\n * answer came back); `code` is the error body's `code` when the server sent one.\n */\nexport class RemoteDatasetError extends Error {\n /** The data server base url ('' when the configured url is empty). */\n readonly url: string;\n /** The HTTP status; 0 = no HTTP answer. */\n readonly status: number;\n /** The error body's `code`, when the server sent one. */\n readonly code?: string;\n\n constructor(message: string, url: string, status: number, code?: string) {\n super(message);\n this.name = new.target.name;\n this.url = url;\n this.status = status;\n if (code !== undefined) this.code = code;\n }\n}\n\n/**\n * Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an\n * answer that is not the server's. A boot that gets this should stop.\n */\nexport class RemoteDatasetUnreachableError extends RemoteDatasetError {}\n\n/** The data server answered 401 or 403: it refused this dataset's token. */\nexport class RemoteDatasetUnauthorizedError extends RemoteDatasetError {}\n\n// Branded, so `isLinkedError(err, RemoteDatasetUnreachableError)` from `@_linked/core` matches an\n// error thrown by another copy of this module, where `instanceof` does not. See\n// https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\nbrandLinkedError(RemoteDatasetError, '@_linked/server', 'RemoteDatasetError');\nbrandLinkedError(RemoteDatasetUnreachableError, '@_linked/server', 'RemoteDatasetUnreachableError');\nbrandLinkedError(RemoteDatasetUnauthorizedError, '@_linked/server', 'RemoteDatasetUnauthorizedError');\n\n/**\n * What the data server's `ping` answered, as is. The generic `@_linked/server` ping answers\n * `{ok: true}`; a gateway may add fields that say which dataset it serves, such as\n * `{ok, projectId, branchName, dataset, forked}`. A body that is not a JSON object reads as `{}`.\n */\nexport type RemoteDatasetPingInfo = Readonly<Record<string, unknown>>;\n\ntype Method =\n | 'ping'\n | 'selectQuery'\n | 'askQuery'\n | 'updateQuery'\n | 'createQuery'\n | 'deleteQuery';\n\n/** On `ping`, these statuses mean nothing at that url serves the query wire. */\nconst PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);\n\n/**\n * An {@link IDataset} whose queries are answered by a linked server.\n *\n * - **Without `url`** the server is this app's own backend: every query goes through\n * `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),\n * and on a backend `Server`'s local shortcut answers it in process. This is the store a\n * browser uses to talk to its backend.\n * - **With `url`** the server is a remote data server: every query posts\n * `{\"args\": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer\n * `token`, through `fetch`, never through `Server` (whose local shortcut would answer the\n * app's own provider) and never retried (a retried write could apply twice). `init()` asks\n * the server's `ping` and throws {@link RemoteDatasetUnreachableError} or\n * {@link RemoteDatasetUnauthorizedError} when it cannot be used.\n *\n * A store is not a Shape: it addresses the backend by package name, so nothing about the\n * store instance crosses the wire. Query routing is decided by the query's shape.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself first; a successful `init()` is memoized, and resolves\n * the ping body (also readable as `info`), so a boot can log which dataset the server serves.\n */\nexport class RemoteDataset implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named; it is not an\n * addressing mechanism, nothing transmits or resolves it.\n */\n readonly id?: string;\n /** The data server base url, trailing slashes trimmed; undefined in the no-url form. */\n readonly url?: string;\n private readonly token?: string;\n private initialized?: Promise<RemoteDatasetPingInfo | undefined>;\n private pingInfo?: RemoteDatasetPingInfo;\n\n constructor(config?: RemoteDatasetConfig | string) {\n if (!config) return;\n if (typeof config === 'string') {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;\n return;\n }\n if (typeof config.url === 'string') {\n this.url = config.url.trim().replace(/\\/+$/, '');\n const token = typeof config.token === 'string' ? config.token.trim() : '';\n if (token) this.token = token;\n }\n if (config.id) {\n this.id = config.id;\n return;\n }\n if (config.name) {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;\n }\n }\n\n /**\n * The body of the last successful `ping` (see {@link init}); undefined before one, and\n * always in the no-url form.\n */\n get info(): RemoteDatasetPingInfo | undefined {\n return this.pingInfo;\n }\n\n /**\n * In the url form, asks the server's `ping` and resolves its body (also kept as\n * {@link info}); resolves undefined in the no-url form.\n */\n init(): Promise<RemoteDatasetPingInfo | undefined> {\n // No initialization needed when queries are routed to the app's own backend.\n if (this.url === undefined) return Promise.resolve(undefined);\n if (!this.initialized) {\n this.initialized = this.post('ping', []).then(\n (body) => {\n const info: RemoteDatasetPingInfo = Object.freeze(\n body && typeof body === 'object' && !Array.isArray(body)\n ? { ...(body as Record<string, unknown>) }\n : {},\n );\n this.pingInfo = info;\n return info;\n },\n (err) => {\n // Not memoized: the next init() asks again.\n this.initialized = undefined;\n throw err;\n },\n );\n }\n return this.initialized;\n }\n\n // Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call\n // as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with\n // `fromJSON()`.\n selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', query.toJSON());\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.call('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.call('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.call('deleteQuery', query.toJSON());\n }\n\n private call<T>(method: Method, json: unknown): Promise<T> {\n if (this.url === undefined) return this.callBackend(method, json);\n return this.post(method, [json]) as Promise<T>;\n }\n\n /**\n * Opts in to `rejectOnError`, so a failed call (HTTP error status, or no\n * provider on the backend) rejects with a `ServerCallError` carrying the\n * status and the server's message instead of reading as an empty result.\n * Whatever a successful call returns, `undefined` included, resolves as is.\n */\n private callBackend<T>(method: string, json: unknown): Promise<T> {\n // The PACKAGE form, not the shape form. It posts to\n // `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,\n // no `instanceNode` — and the backend answers from this package's generic\n // provider without resolving or instantiating any Shape.\n return Server.call(packageName, { method, rejectOnError: true }, json);\n }\n\n /**\n * The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed\n * body (`null` is a valid result: a method that returned `undefined`). Messages name the\n * url and the method, never the token.\n */\n private async post(method: Method, args: unknown[]): Promise<unknown> {\n const base = this.url ?? '';\n if (!base) {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`,\n '',\n 0,\n );\n }\n const target = `${base}/call/${packageName}/${method}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n if (this.token) headers.Authorization = `Bearer ${this.token}`;\n\n let response: Response;\n try {\n response = await globalThis.fetch(target, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args }),\n });\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`,\n base,\n 0,\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n let isJson = false;\n if (text) {\n try {\n body = JSON.parse(text);\n isJson = true;\n } catch {\n body = text;\n }\n }\n\n if (response.ok) {\n if (isJson) return body;\n if (method === 'ping') {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`,\n base,\n response.status,\n );\n }\n if (!text) return undefined;\n throw new RemoteDatasetError(\n `RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`,\n base,\n response.status,\n );\n }\n\n const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : undefined;\n const code = typeof record?.code === 'string' ? record.code : undefined;\n const detail =\n typeof record?.error === 'string'\n ? record.error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${\n code ? ` (${code})` : ''\n }${detail ? `: ${detail}` : ''}`;\n\n if (response.status === 401 || response.status === 403) {\n throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);\n }\n if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {\n throw new RemoteDatasetUnreachableError(message, base, response.status, code);\n }\n throw new RemoteDatasetError(message, base, response.status, code);\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { brandLinkedError } from '@_linked/core/utils/linkedError';
|
|
1
2
|
/** An error that refuses to let the server start. See {@link FatalError}. */
|
|
2
3
|
export class FatalStartupError extends Error {
|
|
3
4
|
constructor(message, options) {
|
|
@@ -12,6 +13,7 @@ export class FatalStartupError extends Error {
|
|
|
12
13
|
this.fatal = true;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
16
|
+
brandLinkedError(FatalStartupError, '@_linked/server', 'FatalStartupError');
|
|
15
17
|
/** True for an error that carries the fatal contract: own `fatal === true`. */
|
|
16
18
|
export function isFatalError(err) {
|
|
17
19
|
return (!!err &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fatalError.js","sourceRoot":"","sources":["../../../src/utils/fatalError.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fatalError.js","sourceRoot":"","sources":["../../../src/utils/fatalError.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AA8BnE,6EAA6E;AAC7E,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAE1C,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,0EAA0E;QAC1E,6BAA6B;QAC7B,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO;YAAG,IAAY,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACvE,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,uEAAuE;QACvE,iCAAiC;QACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;CACF;AAED,gBAAgB,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,CAAC,CAAC;AAE5E,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,CACL,CAAC,CAAC,GAAG;QACL,OAAO,GAAG,KAAK,QAAQ;QACvB,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC;QACjD,GAAW,CAAC,KAAK,KAAK,IAAI,CAC5B,CAAC;AACJ,CAAC","sourcesContent":["import { brandLinkedError } from '@_linked/core/utils/linkedError';\n\n/**\n * The fatal-error contract between providers and the server's boot.\n *\n * The server isolates provider failures: a provider constructor, a backend\n * module load or a boot hook that throws is logged with the package name and\n * boot carries on without that provider. That is right for a bug, and wrong\n * for a provider that has decided the app must not serve at all — auth with\n * no signing secret in production, say. Serving anyway looks healthy and\n * fails every request that needs it.\n *\n * So: an error with an OWN property `fatal` set to `true` is not swallowed\n * when it is thrown at boot. The server logs it, `start()` rejects with it,\n * and the process exits non-zero. Any error object qualifies — a provider\n * does not need to depend on this package to use the contract:\n *\n * throw Object.assign(new Error('JWT_SECRET is not set'), {fatal: true});\n *\n * or, with this package available:\n *\n * throw new FatalStartupError('JWT_SECRET is not set');\n *\n * Thrown after boot (a per-request hook, a lazy load on a later /call, an\n * HMR reload) it is logged as fatal but does not stop a running server.\n */\nexport interface FatalError extends Error {\n fatal: true;\n}\n\n/** An error that refuses to let the server start. See {@link FatalError}. */\nexport class FatalStartupError extends Error implements FatalError {\n fatal: true;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message);\n // `cause` by hand: the ES2022 Error(message, options) signature is not in\n // this package's lib target.\n if (options && 'cause' in options) (this as any).cause = options.cause;\n this.name = 'FatalStartupError';\n // Assigned, not declared with an initializer, so it is an own property\n // whatever the class-field emit.\n this.fatal = true;\n }\n}\n\nbrandLinkedError(FatalStartupError, '@_linked/server', 'FatalStartupError');\n\n/** True for an error that carries the fatal contract: own `fatal === true`. */\nexport function isFatalError(err: unknown): err is FatalError {\n return (\n !!err &&\n typeof err === 'object' &&\n Object.prototype.hasOwnProperty.call(err, 'fatal') &&\n (err as any).fatal === true\n );\n}\n"]}
|
|
@@ -23,6 +23,7 @@ import { checkQueryAccess, } from '@_linked/server-utils/utils/QueryAccess';
|
|
|
23
23
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
24
24
|
import { getRawQueriesMode } from './rawQueries.js';
|
|
25
25
|
import { getRpcExposureMode } from './rpcExposure.js';
|
|
26
|
+
import { sharedState } from '@_linked/core/utils/sharedState';
|
|
26
27
|
/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */
|
|
27
28
|
const CONTEXT_REF_KEY = '@ctx';
|
|
28
29
|
/**
|
|
@@ -51,14 +52,21 @@ export function containsContextRef(json) {
|
|
|
51
52
|
}
|
|
52
53
|
return false;
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The host's resolver, shared by every copy of this module in the process: the host sets it
|
|
57
|
+
* through one copy, and the query endpoint served by another must use it rather than decode in
|
|
58
|
+
* the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.
|
|
59
|
+
*/
|
|
60
|
+
const queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({
|
|
61
|
+
catalogResolver: undefined,
|
|
62
|
+
}));
|
|
55
63
|
/**
|
|
56
64
|
* Set (or, with `undefined`, clear) the host's catalog resolver for the
|
|
57
65
|
* generic query endpoints. Without one, every query decodes in the global
|
|
58
66
|
* shape registry.
|
|
59
67
|
*/
|
|
60
68
|
export function setQueryCatalogResolver(resolver) {
|
|
61
|
-
|
|
69
|
+
queryPlaneState.catalogResolver = resolver;
|
|
62
70
|
}
|
|
63
71
|
/** A live builder passed by backend code (never a plain object from JSON). */
|
|
64
72
|
function isLiveBuilder(query) {
|
|
@@ -76,7 +84,7 @@ function isLiveBuilder(query) {
|
|
|
76
84
|
* the status); any other failure is logged and answers 500.
|
|
77
85
|
*/
|
|
78
86
|
export async function resolveQueryCatalog(query) {
|
|
79
|
-
const resolver =
|
|
87
|
+
const resolver = queryPlaneState.catalogResolver;
|
|
80
88
|
if (!resolver || isLiveBuilder(query))
|
|
81
89
|
return undefined;
|
|
82
90
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport { lower } from '@_linked/core/queries/lower';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\nlet queryCatalogResolver: QueryCatalogResolver | undefined;\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryCatalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryCatalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport { lower } from '@_linked/core/queries/lower';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\n/**\n * The host's resolver, shared by every copy of this module in the process: the host sets it\n * through one copy, and the query endpoint served by another must use it rather than decode in\n * the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({\n catalogResolver: undefined as QueryCatalogResolver | undefined,\n}));\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryPlaneState.catalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryPlaneState.catalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n"]}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { sharedState } from '@_linked/core/utils/sharedState';
|
|
2
|
+
/**
|
|
3
|
+
* The configured mode, shared by every copy of this module in the process: `LinkedServer` sets
|
|
4
|
+
* it from config through one copy, and the query plane on another copy must not fall back to
|
|
5
|
+
* the permissive default. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.
|
|
6
|
+
*/
|
|
7
|
+
const rawQueriesState = sharedState('@_linked/server', 'rawQueries', () => ({
|
|
8
|
+
mode: undefined,
|
|
9
|
+
}));
|
|
2
10
|
let invalidEnvWarned = false;
|
|
3
11
|
function parseMode(value) {
|
|
4
12
|
return value === 'off' || value === 'session' ? value : undefined;
|
|
@@ -9,24 +17,24 @@ function parseMode(value) {
|
|
|
9
17
|
*/
|
|
10
18
|
export function setRawQueriesMode(mode) {
|
|
11
19
|
if (mode === undefined || mode === null) {
|
|
12
|
-
|
|
20
|
+
rawQueriesState.mode = undefined;
|
|
13
21
|
return;
|
|
14
22
|
}
|
|
15
23
|
const parsed = parseMode(mode);
|
|
16
24
|
if (!parsed) {
|
|
17
25
|
console.warn(`[linked] server.rawQueries must be 'off' or 'session', got ${JSON.stringify(mode)}; raw queries are off`);
|
|
18
|
-
|
|
26
|
+
rawQueriesState.mode = 'off';
|
|
19
27
|
return;
|
|
20
28
|
}
|
|
21
|
-
|
|
29
|
+
rawQueriesState.mode = parsed;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* `server.rawQueries`, else `LINKED_RAW_QUERIES`, else `'session'`. An invalid
|
|
25
33
|
* `LINKED_RAW_QUERIES` turns raw queries off.
|
|
26
34
|
*/
|
|
27
35
|
export function getRawQueriesMode() {
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
36
|
+
if (rawQueriesState.mode)
|
|
37
|
+
return rawQueriesState.mode;
|
|
30
38
|
const env = process.env.LINKED_RAW_QUERIES;
|
|
31
39
|
if (env === undefined || env === '')
|
|
32
40
|
return 'session';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rawQueries.js","sourceRoot":"","sources":["../../../src/utils/rawQueries.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rawQueries.js","sourceRoot":"","sources":["../../../src/utils/rawQueries.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAI9D;;;;GAIG;AACH,MAAM,eAAe,GAAG,WAAW,CAAC,iBAAiB,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1E,IAAI,EAAE,SAAuC;CAC9C,CAAC,CAAC,CAAC;AACJ,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QACxC,eAAe,CAAC,IAAI,GAAG,SAAS,CAAC;QACjC,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CACV,8DAA8D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAC1G,CAAC;QACF,eAAe,CAAC,IAAI,GAAG,KAAK,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,eAAe,CAAC,IAAI,GAAG,MAAM,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,eAAe,CAAC,IAAI;QAAE,OAAO,eAAe,CAAC,IAAI,CAAC;IACtD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC3C,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,gBAAgB,GAAG,IAAI,CAAC;QACxB,OAAO,CAAC,IAAI,CACV,+DAA+D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAC1G,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Who may run raw SPARQL through the generic query plane (`/api/select-raw`).\n *\n * Raw SPARQL cannot be analysed for the shapes it touches, so the per-store\n * access rules of `@_linked/server-utils/utils/QueryAccess` do not apply to\n * it. This one setting decides instead:\n *\n * - `'session'` (the default): any signed-in session may run it; 401 without one.\n * - `'off'`: every raw query is refused (403).\n *\n * Set it with `server.rawQueries` in the app's config, or `LINKED_RAW_QUERIES`.\n */\nimport type { RawQueriesMode } from '@_linked/server-utils/utils/QueryAccess';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\nexport type { RawQueriesMode };\n\n/**\n * The configured mode, shared by every copy of this module in the process: `LinkedServer` sets\n * it from config through one copy, and the query plane on another copy must not fall back to\n * the permissive default. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst rawQueriesState = sharedState('@_linked/server', 'rawQueries', () => ({\n mode: undefined as RawQueriesMode | undefined,\n}));\nlet invalidEnvWarned = false;\n\nfunction parseMode(value: unknown): RawQueriesMode | undefined {\n return value === 'off' || value === 'session' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rawQueries`). An invalid value turns\n * raw queries off, with a warning: a typo must not open them up.\n */\nexport function setRawQueriesMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n rawQueriesState.mode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rawQueries must be 'off' or 'session', got ${JSON.stringify(mode)}; raw queries are off`\n );\n rawQueriesState.mode = 'off';\n return;\n }\n rawQueriesState.mode = parsed;\n}\n\n/**\n * `server.rawQueries`, else `LINKED_RAW_QUERIES`, else `'session'`. An invalid\n * `LINKED_RAW_QUERIES` turns raw queries off.\n */\nexport function getRawQueriesMode(): RawQueriesMode {\n if (rawQueriesState.mode) return rawQueriesState.mode;\n const env = process.env.LINKED_RAW_QUERIES;\n if (env === undefined || env === '') return 'session';\n const parsed = parseMode(env);\n if (parsed) return parsed;\n if (!invalidEnvWarned) {\n invalidEnvWarned = true;\n console.warn(\n `[linked] LINKED_RAW_QUERIES must be 'off' or 'session', got ${JSON.stringify(env)}; raw queries are off`\n );\n }\n return 'off';\n}\n"]}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
|
|
37
37
|
import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
|
|
38
38
|
import { getOwnCallableLevel, isDeclaredInternal, } from '@_linked/server-utils/utils/callable';
|
|
39
|
+
import { sharedState } from '@_linked/core/utils/sharedState';
|
|
39
40
|
function ownNames(proto) {
|
|
40
41
|
return proto ? Object.getOwnPropertyNames(proto) : [];
|
|
41
42
|
}
|
|
@@ -155,7 +156,14 @@ export function isGenericRpcDisabled(provider) {
|
|
|
155
156
|
Object.prototype.hasOwnProperty.call(cls, 'rpc') &&
|
|
156
157
|
cls.rpc === false);
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The configured mode, shared by every copy of this module in the process: `LinkedServer` sets
|
|
161
|
+
* it from config through one copy, and the dispatcher on another copy must not fall back to
|
|
162
|
+
* `'warn'`. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.
|
|
163
|
+
*/
|
|
164
|
+
const rpcExposureState = sharedState('@_linked/server', 'rpcExposure', () => ({
|
|
165
|
+
mode: undefined,
|
|
166
|
+
}));
|
|
159
167
|
function parseMode(value) {
|
|
160
168
|
return value === 'warn' || value === 'enforce' ? value : undefined;
|
|
161
169
|
}
|
|
@@ -165,7 +173,7 @@ function parseMode(value) {
|
|
|
165
173
|
*/
|
|
166
174
|
export function setRpcExposureMode(mode) {
|
|
167
175
|
if (mode === undefined || mode === null) {
|
|
168
|
-
|
|
176
|
+
rpcExposureState.mode = undefined;
|
|
169
177
|
return;
|
|
170
178
|
}
|
|
171
179
|
const parsed = parseMode(mode);
|
|
@@ -173,12 +181,12 @@ export function setRpcExposureMode(mode) {
|
|
|
173
181
|
console.warn(`[linked] server.rpcExposure must be 'warn' or 'enforce', got ${JSON.stringify(mode)}; ignoring it`);
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
176
|
-
|
|
184
|
+
rpcExposureState.mode = parsed;
|
|
177
185
|
}
|
|
178
186
|
/** `server.rpcExposure`, else `LINKED_RPC_EXPOSURE`, else `'warn'`. */
|
|
179
187
|
export function getRpcExposureMode() {
|
|
180
|
-
var _a;
|
|
181
|
-
return (_a =
|
|
188
|
+
var _a, _b;
|
|
189
|
+
return (_b = (_a = rpcExposureState.mode) !== null && _a !== void 0 ? _a : parseMode(process.env.LINKED_RPC_EXPOSURE)) !== null && _b !== void 0 ? _b : 'warn';
|
|
182
190
|
}
|
|
183
191
|
const warnedUndeclared = new Set();
|
|
184
192
|
/** Log an undeclared call once per package, class and method. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpcExposure.js","sourceRoot":"","sources":["../../../src/utils/rpcExposure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which provider methods a `/call/...` request may reach.\n *\n * A provider method is dispatched over HTTP when it is declared with\n * `@callable(level)` (or `declareCallable`) from\n * `@_linked/server-utils/utils/callable`. TypeScript `private`/`protected` do not\n * exist at runtime, so without a declaration every method of a provider would be\n * reachable, including everything it inherits.\n *\n * The class that owns the method (the first prototype on the chain that has it)\n * decides. When that class declares it, its own level applies. When it does not\n * (an override of an inherited method), the method keeps the strictest level\n * declared for it by any class up the chain (`'user'` over `'public'`), so an\n * override never loses a session requirement. Only when no class declares it is\n * the method undeclared.\n *\n * - **reserved** — never dispatched, in any mode: `Object.prototype` members,\n * every `BackendProvider`/`ShapeProvider` method (`initRequest`,\n * `registerRoute`, lifecycle hooks, ...), even when a subclass overrides it,\n * `dispose` (a lifecycle hook the server calls on reload, which the base\n * class does not define), `constructor`, `__proto__`, and anything that is not\n * a plain function (accessors, fields). Answered 501, exactly like a missing\n * method.\n * - **internal** — declared with `@internal()` / `declareInternal` by the\n * provider's class or one of its super classes (an app can declare it on a\n * class it imports). Never dispatched over HTTP, in any mode (501), even when\n * it is also declared callable. Backend-to-backend calls still reach it.\n * - **undeclared** — no class on the chain declares it. In `warn` mode it runs\n * and is logged once; in `enforce` mode it is 501.\n * - **callable** — declared (directly or inherited, see above). A `'user'`\n * method answers 401 without a session.\n *\n * Backend-to-backend calls (`Server.call` on the server) skip everything except\n * the reserved names, internal methods included.\n */\nimport { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';\nimport {\n getOwnCallableLevel,\n isDeclaredInternal,\n type CallableLevel,\n} from '@_linked/server-utils/utils/callable';\n\nexport type RpcExposureMode = 'warn' | 'enforce';\n\nexport type CallableResolution =\n | { status: 'callable'; level: CallableLevel; owner: Function; inherited?: boolean }\n | { status: 'undeclared'; owner: Function | undefined }\n | { status: 'internal'; owner: Function | undefined }\n | { status: 'reserved' }\n | { status: 'missing' };\n\nfunction ownNames(proto: object | undefined): string[] {\n return proto ? Object.getOwnPropertyNames(proto) : [];\n}\n\nlet reservedNames: Set<string> | undefined;\n\n/**\n * Names no RPC may dispatch to. Computed on first use (not at import), so the\n * base classes are fully defined, and including every method they have.\n */\nexport function getReservedNames(): ReadonlySet<string> {\n if (!reservedNames) {\n reservedNames = new Set([\n ...ownNames(Object.prototype),\n ...ownNames(BackendProvider.prototype),\n ...ownNames(ShapeProvider.prototype),\n // called by the server when it reloads providers; no base class defines it\n 'dispose',\n 'constructor',\n '__proto__',\n ]);\n }\n return reservedNames;\n}\n\nfunction isBaseClassPrototype(obj: object): boolean {\n return (\n obj === Object.prototype ||\n obj === BackendProvider.prototype ||\n obj === ShapeProvider.prototype\n );\n}\n\n/**\n * Decide whether `method` of `provider` may be dispatched.\n *\n * Walks the prototype chain with property descriptors only, so no getter ever\n * runs. The first object that owns the name decides: if it is a class\n * prototype, that class's own declaration; if it is the instance itself (a\n * method assigned onto the instance), there is no declaration.\n */\nexport function resolveCallable(provider: any, method: string): CallableResolution {\n if (typeof method !== 'string' || !method) return { status: 'missing' };\n if (provider == null || (typeof provider !== 'object' && typeof provider !== 'function')) {\n return { status: 'missing' };\n }\n const reserved = getReservedNames();\n let obj: any = provider;\n let descriptor: PropertyDescriptor | undefined;\n let holder: any;\n while (obj) {\n descriptor = Object.getOwnPropertyDescriptor(obj, method);\n if (descriptor) {\n holder = obj;\n break;\n }\n obj = Object.getPrototypeOf(obj);\n }\n if (!descriptor) {\n return reserved.has(method) ? { status: 'reserved' } : { status: 'missing' };\n }\n if (reserved.has(method) || isBaseClassPrototype(holder)) {\n return { status: 'reserved' };\n }\n if (!('value' in descriptor) || typeof descriptor.value !== 'function') {\n return { status: 'reserved' };\n }\n const holderCtor = Object.prototype.hasOwnProperty.call(holder, 'constructor')\n ? holder.constructor\n : undefined;\n const isClassPrototype =\n typeof holderCtor === 'function' && holderCtor.prototype === holder;\n // Internal on the provider's class or any super class wins over everything\n // else, a callable declaration included.\n const providerCtor = typeof provider === 'function' ? undefined : provider.constructor;\n if (\n (typeof providerCtor === 'function' && isDeclaredInternal(providerCtor, method)) ||\n (isClassPrototype && isDeclaredInternal(holderCtor, method))\n ) {\n return { status: 'internal', owner: isClassPrototype ? holderCtor : providerCtor };\n }\n if (isClassPrototype) {\n const own = getOwnCallableLevel(holderCtor, method);\n if (own) return { status: 'callable', level: own, owner: holderCtor };\n }\n // Not declared where it is defined (an override, or a method assigned onto\n // the instance): the strictest level declared up the class chain applies.\n const start = isClassPrototype ? holderCtor : providerCtor;\n const inherited = strictestDeclaredLevel(start, method);\n if (inherited) {\n return { status: 'callable', level: inherited, owner: start, inherited: true };\n }\n return { status: 'undeclared', owner: start };\n}\n\nconst STRICTNESS: Record<CallableLevel, number> = { public: 0, user: 1 };\n\n/** The strictest level any class from `cls` up declares for `method`. */\nfunction strictestDeclaredLevel(cls: unknown, method: string): CallableLevel | undefined {\n let level: CallableLevel | undefined;\n let c: any = cls;\n while (typeof c === 'function' && c !== Function.prototype) {\n const own = getOwnCallableLevel(c, method);\n if (own && (level === undefined || STRICTNESS[own] > STRICTNESS[level])) level = own;\n c = Object.getPrototypeOf(c);\n }\n return level;\n}\n\n/**\n * A client-supplied name made safe for a log line: control characters escaped\n * (no forged lines or terminal escapes), and cut to a bounded length.\n */\nexport function logSafe(value: unknown, max = 120): string {\n const text = String(value);\n const escaped = text.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g, (ch) =>\n '\\\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')\n );\n return escaped.length > max ? escaped.slice(0, max) + '…' : escaped;\n}\n\n/**\n * True when the provider's own class opts out of the generic `/call/<pkg>/...`\n * route with `static rpc = false`. Only the provider's own class counts.\n */\nexport function isGenericRpcDisabled(provider: any): boolean {\n const cls = provider?.constructor;\n return (\n typeof cls === 'function' &&\n Object.prototype.hasOwnProperty.call(cls, 'rpc') &&\n (cls as any).rpc === false\n );\n}\n\nlet configuredMode: RpcExposureMode | undefined;\n\nfunction parseMode(value: unknown): RpcExposureMode | undefined {\n return value === 'warn' || value === 'enforce' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rpcExposure`). An invalid value is\n * ignored with a warning, leaving the environment / default in place.\n */\nexport function setRpcExposureMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n configuredMode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rpcExposure must be 'warn' or 'enforce', got ${JSON.stringify(mode)}; ignoring it`\n );\n return;\n }\n configuredMode = parsed;\n}\n\n/** `server.rpcExposure`, else `LINKED_RPC_EXPOSURE`, else `'warn'`. */\nexport function getRpcExposureMode(): RpcExposureMode {\n return configuredMode ?? parseMode(process.env.LINKED_RPC_EXPOSURE) ?? 'warn';\n}\n\nconst warnedUndeclared = new Set<string>();\n\n/** Log an undeclared call once per package, class and method. */\nexport function warnUndeclaredCall(\n pkg: string,\n owner: Function | undefined,\n method: string,\n hasSession: boolean\n): void {\n const className = owner?.name || 'anonymous provider';\n const key = `${pkg}\\u0000${className}\\u0000${method}`;\n if (warnedUndeclared.has(key)) return;\n warnedUndeclared.add(key);\n console.warn(\n `[linked] undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)} (session: ${hasSession ? 'yes' : 'no'}). ` +\n `Declare it with @callable('public' | 'user') if the client calls it; ` +\n `undeclared methods are refused (501) once rpcExposure is 'enforce'.`\n );\n}\n\n/** For tests: forget which undeclared calls were already logged. */\nexport function resetUndeclaredWarnings(): void {\n warnedUndeclared.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpcExposure.js","sourceRoot":"","sources":["../../../src/utils/rpcExposure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which provider methods a `/call/...` request may reach.\n *\n * A provider method is dispatched over HTTP when it is declared with\n * `@callable(level)` (or `declareCallable`) from\n * `@_linked/server-utils/utils/callable`. TypeScript `private`/`protected` do not\n * exist at runtime, so without a declaration every method of a provider would be\n * reachable, including everything it inherits.\n *\n * The class that owns the method (the first prototype on the chain that has it)\n * decides. When that class declares it, its own level applies. When it does not\n * (an override of an inherited method), the method keeps the strictest level\n * declared for it by any class up the chain (`'user'` over `'public'`), so an\n * override never loses a session requirement. Only when no class declares it is\n * the method undeclared.\n *\n * - **reserved** — never dispatched, in any mode: `Object.prototype` members,\n * every `BackendProvider`/`ShapeProvider` method (`initRequest`,\n * `registerRoute`, lifecycle hooks, ...), even when a subclass overrides it,\n * `dispose` (a lifecycle hook the server calls on reload, which the base\n * class does not define), `constructor`, `__proto__`, and anything that is not\n * a plain function (accessors, fields). Answered 501, exactly like a missing\n * method.\n * - **internal** — declared with `@internal()` / `declareInternal` by the\n * provider's class or one of its super classes (an app can declare it on a\n * class it imports). Never dispatched over HTTP, in any mode (501), even when\n * it is also declared callable. Backend-to-backend calls still reach it.\n * - **undeclared** — no class on the chain declares it. In `warn` mode it runs\n * and is logged once; in `enforce` mode it is 501.\n * - **callable** — declared (directly or inherited, see above). A `'user'`\n * method answers 401 without a session.\n *\n * Backend-to-backend calls (`Server.call` on the server) skip everything except\n * the reserved names, internal methods included.\n */\nimport { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';\nimport {\n getOwnCallableLevel,\n isDeclaredInternal,\n type CallableLevel,\n} from '@_linked/server-utils/utils/callable';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\nexport type RpcExposureMode = 'warn' | 'enforce';\n\nexport type CallableResolution =\n | { status: 'callable'; level: CallableLevel; owner: Function; inherited?: boolean }\n | { status: 'undeclared'; owner: Function | undefined }\n | { status: 'internal'; owner: Function | undefined }\n | { status: 'reserved' }\n | { status: 'missing' };\n\nfunction ownNames(proto: object | undefined): string[] {\n return proto ? Object.getOwnPropertyNames(proto) : [];\n}\n\nlet reservedNames: Set<string> | undefined;\n\n/**\n * Names no RPC may dispatch to. Computed on first use (not at import), so the\n * base classes are fully defined, and including every method they have.\n */\nexport function getReservedNames(): ReadonlySet<string> {\n if (!reservedNames) {\n reservedNames = new Set([\n ...ownNames(Object.prototype),\n ...ownNames(BackendProvider.prototype),\n ...ownNames(ShapeProvider.prototype),\n // called by the server when it reloads providers; no base class defines it\n 'dispose',\n 'constructor',\n '__proto__',\n ]);\n }\n return reservedNames;\n}\n\nfunction isBaseClassPrototype(obj: object): boolean {\n return (\n obj === Object.prototype ||\n obj === BackendProvider.prototype ||\n obj === ShapeProvider.prototype\n );\n}\n\n/**\n * Decide whether `method` of `provider` may be dispatched.\n *\n * Walks the prototype chain with property descriptors only, so no getter ever\n * runs. The first object that owns the name decides: if it is a class\n * prototype, that class's own declaration; if it is the instance itself (a\n * method assigned onto the instance), there is no declaration.\n */\nexport function resolveCallable(provider: any, method: string): CallableResolution {\n if (typeof method !== 'string' || !method) return { status: 'missing' };\n if (provider == null || (typeof provider !== 'object' && typeof provider !== 'function')) {\n return { status: 'missing' };\n }\n const reserved = getReservedNames();\n let obj: any = provider;\n let descriptor: PropertyDescriptor | undefined;\n let holder: any;\n while (obj) {\n descriptor = Object.getOwnPropertyDescriptor(obj, method);\n if (descriptor) {\n holder = obj;\n break;\n }\n obj = Object.getPrototypeOf(obj);\n }\n if (!descriptor) {\n return reserved.has(method) ? { status: 'reserved' } : { status: 'missing' };\n }\n if (reserved.has(method) || isBaseClassPrototype(holder)) {\n return { status: 'reserved' };\n }\n if (!('value' in descriptor) || typeof descriptor.value !== 'function') {\n return { status: 'reserved' };\n }\n const holderCtor = Object.prototype.hasOwnProperty.call(holder, 'constructor')\n ? holder.constructor\n : undefined;\n const isClassPrototype =\n typeof holderCtor === 'function' && holderCtor.prototype === holder;\n // Internal on the provider's class or any super class wins over everything\n // else, a callable declaration included.\n const providerCtor = typeof provider === 'function' ? undefined : provider.constructor;\n if (\n (typeof providerCtor === 'function' && isDeclaredInternal(providerCtor, method)) ||\n (isClassPrototype && isDeclaredInternal(holderCtor, method))\n ) {\n return { status: 'internal', owner: isClassPrototype ? holderCtor : providerCtor };\n }\n if (isClassPrototype) {\n const own = getOwnCallableLevel(holderCtor, method);\n if (own) return { status: 'callable', level: own, owner: holderCtor };\n }\n // Not declared where it is defined (an override, or a method assigned onto\n // the instance): the strictest level declared up the class chain applies.\n const start = isClassPrototype ? holderCtor : providerCtor;\n const inherited = strictestDeclaredLevel(start, method);\n if (inherited) {\n return { status: 'callable', level: inherited, owner: start, inherited: true };\n }\n return { status: 'undeclared', owner: start };\n}\n\nconst STRICTNESS: Record<CallableLevel, number> = { public: 0, user: 1 };\n\n/** The strictest level any class from `cls` up declares for `method`. */\nfunction strictestDeclaredLevel(cls: unknown, method: string): CallableLevel | undefined {\n let level: CallableLevel | undefined;\n let c: any = cls;\n while (typeof c === 'function' && c !== Function.prototype) {\n const own = getOwnCallableLevel(c, method);\n if (own && (level === undefined || STRICTNESS[own] > STRICTNESS[level])) level = own;\n c = Object.getPrototypeOf(c);\n }\n return level;\n}\n\n/**\n * A client-supplied name made safe for a log line: control characters escaped\n * (no forged lines or terminal escapes), and cut to a bounded length.\n */\nexport function logSafe(value: unknown, max = 120): string {\n const text = String(value);\n const escaped = text.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g, (ch) =>\n '\\\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')\n );\n return escaped.length > max ? escaped.slice(0, max) + '…' : escaped;\n}\n\n/**\n * True when the provider's own class opts out of the generic `/call/<pkg>/...`\n * route with `static rpc = false`. Only the provider's own class counts.\n */\nexport function isGenericRpcDisabled(provider: any): boolean {\n const cls = provider?.constructor;\n return (\n typeof cls === 'function' &&\n Object.prototype.hasOwnProperty.call(cls, 'rpc') &&\n (cls as any).rpc === false\n );\n}\n\n/**\n * The configured mode, shared by every copy of this module in the process: `LinkedServer` sets\n * it from config through one copy, and the dispatcher on another copy must not fall back to\n * `'warn'`. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst rpcExposureState = sharedState('@_linked/server', 'rpcExposure', () => ({\n mode: undefined as RpcExposureMode | undefined,\n}));\n\nfunction parseMode(value: unknown): RpcExposureMode | undefined {\n return value === 'warn' || value === 'enforce' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rpcExposure`). An invalid value is\n * ignored with a warning, leaving the environment / default in place.\n */\nexport function setRpcExposureMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n rpcExposureState.mode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rpcExposure must be 'warn' or 'enforce', got ${JSON.stringify(mode)}; ignoring it`\n );\n return;\n }\n rpcExposureState.mode = parsed;\n}\n\n/** `server.rpcExposure`, else `LINKED_RPC_EXPOSURE`, else `'warn'`. */\nexport function getRpcExposureMode(): RpcExposureMode {\n return rpcExposureState.mode ?? parseMode(process.env.LINKED_RPC_EXPOSURE) ?? 'warn';\n}\n\nconst warnedUndeclared = new Set<string>();\n\n/** Log an undeclared call once per package, class and method. */\nexport function warnUndeclaredCall(\n pkg: string,\n owner: Function | undefined,\n method: string,\n hasSession: boolean\n): void {\n const className = owner?.name || 'anonymous provider';\n const key = `${pkg}\\u0000${className}\\u0000${method}`;\n if (warnedUndeclared.has(key)) return;\n warnedUndeclared.add(key);\n console.warn(\n `[linked] undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)} (session: ${hasSession ? 'yes' : 'no'}). ` +\n `Declare it with @callable('public' | 'user') if the client calls it; ` +\n `undeclared methods are refused (501) once rpcExposure is 'enforce'.`\n );\n}\n\n/** For tests: forget which undeclared calls were already logged. */\nexport function resetUndeclaredWarnings(): void {\n warnedUndeclared.clear();\n}\n"]}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.3.0",
|
|
14
14
|
"packageManager": "npm@12.2.0",
|
|
15
15
|
"linkedPackage": true,
|
|
16
16
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@_linked/cli": "^1.45.5",
|
|
42
|
-
"@_linked/core": "^2.
|
|
42
|
+
"@_linked/core": "^2.36.0",
|
|
43
43
|
"@_linked/server-utils": "^1.12.0",
|
|
44
44
|
"autoprefixer": "^10.4.18",
|
|
45
45
|
"compression": "^1.7.4",
|
package/readme.md
CHANGED
|
@@ -668,3 +668,15 @@ that it exists before calling it.
|
|
|
668
668
|
|
|
669
669
|
- [ ] Add tests
|
|
670
670
|
- [ ] Refactor `NodeFileStore` to `LocalQuadStore` as to not confuse FileStores with QuadStores
|
|
671
|
+
|
|
672
|
+
## Shared state and error brands
|
|
673
|
+
|
|
674
|
+
A module of this package can be evaluated twice in one process (Vite's SSR runner and Node under
|
|
675
|
+
`linked start`). The settings `LinkedServer` applies from config — `server.rawQueries`,
|
|
676
|
+
`server.rpcExposure` — and the host's `setQueryCatalogResolver` are kept on `globalThis` with
|
|
677
|
+
`sharedState` from `@_linked/core`, so a second copy never falls back to its default. The process
|
|
678
|
+
setup (the cookie-jar `fetch`, the `process` error listeners) runs once per process. The error
|
|
679
|
+
classes `RemoteDatasetError`, `RemoteDatasetUnreachableError`, `RemoteDatasetUnauthorizedError` and
|
|
680
|
+
`FatalStartupError` are branded: match them with `isLinkedError(err, RemoteDatasetUnreachableError)`
|
|
681
|
+
from `@_linked/core`, not `instanceof`. See
|
|
682
|
+
[shared state and error brands](https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md).
|