@fitzzero/quickdraw-core 5.0.1 → 5.1.1
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/README.md +303 -32
- package/dist/{admin-CBlTY0do.d.ts → admin-DKwkIh5i.d.ts} +2 -2
- package/dist/{adminSchemas-zji11V74.d.ts → adminSchemas-2DUA_hZ8.d.ts} +1 -1
- package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
- package/dist/{caller-BqCYnpmC.d.ts → caller-CXTFswBO.d.ts} +27 -5
- package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
- package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
- package/dist/{chunk-O2PWC2YW.js → chunk-OECZPRPW.js} +3 -3
- package/dist/{chunk-3JNMVB2V.js → chunk-QCHKWIRS.js} +2 -2
- package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
- package/dist/chunk-RT6OILOI.js.map +1 -0
- package/dist/{chunk-NS5PUHIZ.js → chunk-RZTP4IEW.js} +130 -23
- package/dist/chunk-RZTP4IEW.js.map +1 -0
- package/dist/{chunk-HQ6WIRE2.js → chunk-SHEGLZEP.js} +4 -4
- package/dist/{chunk-WVIRXYI4.js → chunk-VSQBHPWH.js} +2 -2
- package/dist/{chunk-WVIRXYI4.js.map → chunk-VSQBHPWH.js.map} +1 -1
- package/dist/chunk-XNLWBZIG.js +9 -0
- package/dist/chunk-XNLWBZIG.js.map +1 -0
- package/dist/{chunk-ZACRRYWL.js → chunk-ZIVPHYRG.js} +4 -4
- package/dist/cli/quickdraw-docs.js +241 -27
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +7 -7
- package/dist/client/index.js +4 -4
- package/dist/{clientTypes-DuZW_8mb.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
- package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
- package/dist/{createServer-CGsJlxM8.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
- package/dist/{createTestApp-Cg2CZa1g.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
- package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
- package/dist/{http-D3fSFQ57.d.ts → http-Djj2Inuu.d.ts} +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
- package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
- package/dist/prisma/index.d.ts +4 -4
- package/dist/server/auth/index.d.ts +3 -3
- package/dist/server/index.d.ts +47 -26
- package/dist/server/index.js +62 -36
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +51 -11
- package/dist/server/mcp/index.js +129 -8
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-CoreezAq.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
- package/dist/testing/client.d.ts +13 -13
- package/dist/testing/client.js +5 -5
- package/dist/testing/index.d.ts +156 -12
- package/dist/testing/index.js +1036 -21
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +4 -4
- package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-5YRQ26LF.js.map +0 -1
- package/dist/chunk-NS5PUHIZ.js.map +0 -1
- /package/dist/{chunk-O2PWC2YW.js.map → chunk-OECZPRPW.js.map} +0 -0
- /package/dist/{chunk-3JNMVB2V.js.map → chunk-QCHKWIRS.js.map} +0 -0
- /package/dist/{chunk-HQ6WIRE2.js.map → chunk-SHEGLZEP.js.map} +0 -0
- /package/dist/{chunk-ZACRRYWL.js.map → chunk-ZIVPHYRG.js.map} +0 -0
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
searchTargetOf,
|
|
14
14
|
streamMaxOf,
|
|
15
15
|
viewPredicate
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OECZPRPW.js";
|
|
17
17
|
import {
|
|
18
18
|
MAX_SUBSCRIBE_IDS,
|
|
19
19
|
PROTOCOL_VERSION,
|
|
20
20
|
QUICKDRAW_VERSION
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-VSQBHPWH.js";
|
|
22
22
|
import {
|
|
23
23
|
isScopedStream
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-RT6OILOI.js";
|
|
25
25
|
import {
|
|
26
26
|
buildCaller,
|
|
27
27
|
methodKey,
|
|
@@ -849,4 +849,4 @@ function createMockClient(contracts, options = {}) {
|
|
|
849
849
|
export {
|
|
850
850
|
createMockClient
|
|
851
851
|
};
|
|
852
|
-
//# sourceMappingURL=chunk-
|
|
852
|
+
//# sourceMappingURL=chunk-SHEGLZEP.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-4YO5H5ON.js";
|
|
4
4
|
|
|
5
5
|
// src/version.ts
|
|
6
|
-
var QUICKDRAW_VERSION = "5.
|
|
6
|
+
var QUICKDRAW_VERSION = "5.1.1";
|
|
7
7
|
|
|
8
8
|
// src/contract/names.ts
|
|
9
9
|
function entityRoom(service, id, level) {
|
|
@@ -127,4 +127,4 @@ export {
|
|
|
127
127
|
MAX_SUBSCRIBE_IDS,
|
|
128
128
|
MAX_SCOPE_LENGTH
|
|
129
129
|
};
|
|
130
|
-
//# sourceMappingURL=chunk-
|
|
130
|
+
//# sourceMappingURL=chunk-VSQBHPWH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts","../src/contract/access.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.0.1\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (4.1 `src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /**\n * This server's id: random, new each time a server starts. A client that\n * reconnects to the same id came back after a blip, to another id to a\n * restarted server (a game's world and clock start over) or, behind a\n * cluster, to another node, each of which has its own.\n */\n readonly serverId: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n","// Access levels and access-control lists, unchanged from 4.1\n// (4.1 `src/shared/types.ts:7-14`). The level names are stored in app data\n// (`User.serviceAccess`, JSON access lists), so they never change.\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\n/** Every access level, lowest first. */\nexport const ACCESS_LEVELS: readonly AccessLevel[] = Object.freeze([\n \"Public\",\n \"Read\",\n \"Moderate\",\n \"Admin\",\n]);\n\n/** True when `value` is one of the four access level names. */\nexport function isAccessLevel(value: unknown): value is AccessLevel {\n return typeof value === \"string\" && (ACCESS_LEVELS as readonly string[]).includes(value);\n}\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;;;ACpEzB,IAAM,gBAAwC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts","../src/contract/access.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.1.1\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (4.1 `src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /**\n * This server's id: random, new each time a server starts. A client that\n * reconnects to the same id came back after a blip, to another id to a\n * restarted server (a game's world and clock start over) or, behind a\n * cluster, to another node, each of which has its own.\n */\n readonly serverId: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n","// Access levels and access-control lists, unchanged from 4.1\n// (4.1 `src/shared/types.ts:7-14`). The level names are stored in app data\n// (`User.serviceAccess`, JSON access lists), so they never change.\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\n/** Every access level, lowest first. */\nexport const ACCESS_LEVELS: readonly AccessLevel[] = Object.freeze([\n \"Public\",\n \"Read\",\n \"Moderate\",\n \"Admin\",\n]);\n\n/** True when `value` is one of the four access level names. */\nexport function isAccessLevel(value: unknown): value is AccessLevel {\n return typeof value === \"string\" && (ACCESS_LEVELS as readonly string[]).includes(value);\n}\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;;;ACpEzB,IAAM,gBAAwC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contract/toolName.ts"],"sourcesContent":["// A method tool's default name. It lives with the contracts, not in the MCP\n// bridge, so `quickdraw-docs` prints it without the bridge's entry: the\n// bridge's own files ship in `./server/mcp` alone (scripts/dist-smoke.mjs).\n\n/**\n * The name the MCP bridge gives a method's tool when the registry has no\n * `name` option: `{service}_{method}` (RFC 0003 section 10).\n */\nexport function defaultToolName(service: string, method: string): string {\n return `${service}_${method}`;\n}\n"],"mappings":";AAQO,SAAS,gBAAgB,SAAiB,QAAwB;AACvE,SAAO,GAAG,OAAO,IAAI,MAAM;AAC7B;","names":[]}
|
|
@@ -43,15 +43,15 @@ import {
|
|
|
43
43
|
useQuickdrawContext,
|
|
44
44
|
userOf,
|
|
45
45
|
viewPredicate
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-OECZPRPW.js";
|
|
47
47
|
import {
|
|
48
48
|
CLIENT_EVENTS,
|
|
49
49
|
SERVICE_TOPIC,
|
|
50
50
|
collectionTopic
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-VSQBHPWH.js";
|
|
52
52
|
import {
|
|
53
53
|
isScopedStream
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-RT6OILOI.js";
|
|
55
55
|
import {
|
|
56
56
|
buildCaller,
|
|
57
57
|
collectionKey,
|
|
@@ -1410,4 +1410,4 @@ export {
|
|
|
1410
1410
|
QuickdrawProvider,
|
|
1411
1411
|
useQuickdraw
|
|
1412
1412
|
};
|
|
1413
|
-
//# sourceMappingURL=chunk-
|
|
1413
|
+
//# sourceMappingURL=chunk-ZIVPHYRG.js.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
defaultToolName
|
|
4
|
+
} from "../chunk-XNLWBZIG.js";
|
|
2
5
|
import {
|
|
3
6
|
CHANNEL_DEFAULT_RATE,
|
|
4
7
|
DEFAULT_COLLECTION_LIMIT,
|
|
5
8
|
DEFAULT_COLLECTION_MAX_LIMIT,
|
|
6
9
|
isScopedStream
|
|
7
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-RT6OILOI.js";
|
|
8
11
|
import {
|
|
9
12
|
hasJsonSchema,
|
|
10
13
|
isStandardSchema
|
|
@@ -76,6 +79,12 @@ function recordsOf(value) {
|
|
|
76
79
|
}
|
|
77
80
|
return value;
|
|
78
81
|
}
|
|
82
|
+
function kindsOf(value) {
|
|
83
|
+
return Array.isArray(value) && value.every((kind) => typeof kind === "string") ? value : void 0;
|
|
84
|
+
}
|
|
85
|
+
function namesOf(value) {
|
|
86
|
+
return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
|
|
87
|
+
}
|
|
79
88
|
function mapEach(records, read) {
|
|
80
89
|
return new Map([...records].map(([name, record]) => [name, read(record)]));
|
|
81
90
|
}
|
|
@@ -94,13 +103,17 @@ function serviceDocOf(value) {
|
|
|
94
103
|
}
|
|
95
104
|
return Object.freeze({
|
|
96
105
|
name: value.name,
|
|
106
|
+
model: typeof value.model === "string" ? value.model : void 0,
|
|
107
|
+
writes: namesOf(value.writes),
|
|
108
|
+
affects: Array.isArray(value.affects) ? value.affects.map((link) => contractName(isRecord(link) ? link.service : void 0)).filter((name) => name !== void 0) : [],
|
|
97
109
|
policy: value.access,
|
|
98
110
|
adminBypass: value.adminBypass !== false,
|
|
111
|
+
kinds: kindsOf(value.kinds),
|
|
99
112
|
watchAccess: value.watchAccess,
|
|
100
113
|
methods: new Map(
|
|
101
114
|
Object.entries(methods).map(([name, method]) => [
|
|
102
115
|
name,
|
|
103
|
-
{ access: method.access, rowless: method.rowless === true }
|
|
116
|
+
{ access: method.access, rowless: method.rowless === true, kinds: kindsOf(method.kinds) }
|
|
104
117
|
])
|
|
105
118
|
),
|
|
106
119
|
collections: mapEach(collections, (collection) => ({
|
|
@@ -140,8 +153,11 @@ function servicesOf(exports) {
|
|
|
140
153
|
[...found].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, { doc }]) => [name, doc])
|
|
141
154
|
);
|
|
142
155
|
}
|
|
156
|
+
function contractName(contract) {
|
|
157
|
+
return isRecord(contract) && typeof contract.name === "string" ? contract.name : void 0;
|
|
158
|
+
}
|
|
143
159
|
function serviceName(contract) {
|
|
144
|
-
return
|
|
160
|
+
return contractName(contract) ?? "another service";
|
|
145
161
|
}
|
|
146
162
|
function idOf(id) {
|
|
147
163
|
if (id === void 0) {
|
|
@@ -220,13 +236,164 @@ function collectionAccessText(collection) {
|
|
|
220
236
|
return `${String(collection.level)} or more on the ${code(serviceName(collection.anchor))} row the scope names`;
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
// src/cli/dependencies.ts
|
|
240
|
+
var KINDS = ["inherit", "writes", "affects", "scope", "anchor"];
|
|
241
|
+
function isRecord2(value) {
|
|
242
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
243
|
+
}
|
|
244
|
+
function modelKey(model) {
|
|
245
|
+
return `${model.charAt(0).toLowerCase()}${model.slice(1)}`;
|
|
246
|
+
}
|
|
247
|
+
var MAX_POLICY_DEPTH2 = 3;
|
|
248
|
+
function inheritsOf(policy, depth = 0) {
|
|
249
|
+
if (!isRecord2(policy)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
if (policy.kind === "inherit") {
|
|
253
|
+
const from = contractName(policy.from);
|
|
254
|
+
return from === void 0 ? [] : [{ from, via: String(policy.via) }];
|
|
255
|
+
}
|
|
256
|
+
if (policy.kind === "anyOf" && Array.isArray(policy.policies) && depth < MAX_POLICY_DEPTH2) {
|
|
257
|
+
return policy.policies.flatMap((member) => inheritsOf(member, depth + 1));
|
|
258
|
+
}
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
function scopeOf(form) {
|
|
262
|
+
return isRecord2(form) && typeof form.scope === "string" ? contractName(form.of) : void 0;
|
|
263
|
+
}
|
|
264
|
+
function dependenciesOfService(service, contract, owners) {
|
|
265
|
+
const found = /* @__PURE__ */ new Map();
|
|
266
|
+
const add = (target, kind, text) => {
|
|
267
|
+
if (target === void 0 || target === service.name) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
found.set(target, [...found.get(target) ?? [], { kind, text }]);
|
|
271
|
+
};
|
|
272
|
+
for (const { from, via } of inheritsOf(service.policy)) {
|
|
273
|
+
add(from, "inherit", `row policy: ${code("inherit")} through the ${code(via)} column`);
|
|
274
|
+
}
|
|
275
|
+
const models = [];
|
|
276
|
+
for (const model of service.writes) {
|
|
277
|
+
const serving = owners.get(modelKey(model)) ?? [];
|
|
278
|
+
if (serving.length === 0) {
|
|
279
|
+
models.push(model);
|
|
280
|
+
}
|
|
281
|
+
for (const owner of serving) {
|
|
282
|
+
add(owner, "writes", `${code("writes")}: its ${code(model)} model`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const target of new Set(service.affects)) {
|
|
286
|
+
add(target, "affects", `${code("affects")}: a write to a row here changes its rows`);
|
|
287
|
+
}
|
|
288
|
+
for (const [name, method] of service.methods) {
|
|
289
|
+
add(scopeOf(method.access), "scope", `method ${code(name)}: ${code("{ scope, of }")}`);
|
|
290
|
+
}
|
|
291
|
+
for (const [name, access] of service.channels) {
|
|
292
|
+
add(scopeOf(access), "scope", `channel ${code(name)}: ${code("{ scope, of }")}`);
|
|
293
|
+
}
|
|
294
|
+
for (const [name, stream] of Object.entries(contract?.streams ?? {})) {
|
|
295
|
+
add(scopeOf(stream.access), "scope", `stream ${code(name)}: ${code("{ scope, of }")}`);
|
|
296
|
+
}
|
|
297
|
+
for (const [name, collection] of service.collections) {
|
|
298
|
+
add(
|
|
299
|
+
contractName(collection.anchor),
|
|
300
|
+
"anchor",
|
|
301
|
+
`collection ${code(name)}: anchored on its rows`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
services: [...found].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, reasons]) => ({
|
|
306
|
+
service: name,
|
|
307
|
+
reasons: KINDS.flatMap((kind) => reasons.filter((reason) => reason.kind === kind))
|
|
308
|
+
})),
|
|
309
|
+
models
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function dependenciesOf(contracts, services) {
|
|
313
|
+
const owners = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const service of services.values()) {
|
|
315
|
+
if (service.model !== void 0) {
|
|
316
|
+
const key = modelKey(service.model);
|
|
317
|
+
owners.set(key, [...owners.get(key) ?? [], service.name]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const byName = new Map(contracts.map((contract) => [contract.name, contract]));
|
|
321
|
+
return new Map(
|
|
322
|
+
[...services.values()].map((service) => [
|
|
323
|
+
service.name,
|
|
324
|
+
dependenciesOfService(service, byName.get(service.name), owners)
|
|
325
|
+
])
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
function kindsOf2(dependency) {
|
|
329
|
+
return KINDS.filter((kind) => dependency.reasons.some((reason) => reason.kind === kind));
|
|
330
|
+
}
|
|
331
|
+
function dependsOnSection(dependencies) {
|
|
332
|
+
const { services, models } = dependencies;
|
|
333
|
+
const unserved = models.length === 0 ? [] : [`It also ${code("writes")} models no service's rows live in: ${list(models)}.`];
|
|
334
|
+
if (services.length === 0) {
|
|
335
|
+
return section("## Depends on", ["No other service: its declarations name none.", ...unserved]);
|
|
336
|
+
}
|
|
337
|
+
return section("## Depends on", [
|
|
338
|
+
`The other services its declarations name: its row policy, ${code("writes")}, ${code("affects")}, ${code("{ scope, of }")} access forms and collection anchors. A handler's undeclared reads are not listed.`,
|
|
339
|
+
table(
|
|
340
|
+
["Service", "Declared by"],
|
|
341
|
+
services.map(({ service, reasons }) => [
|
|
342
|
+
code(service),
|
|
343
|
+
reasons.map((reason) => reason.text).join("; ")
|
|
344
|
+
])
|
|
345
|
+
),
|
|
346
|
+
...unserved
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
function nodeIds(names) {
|
|
350
|
+
const ids = /* @__PURE__ */ new Map();
|
|
351
|
+
const taken = /* @__PURE__ */ new Set();
|
|
352
|
+
for (const name of names) {
|
|
353
|
+
const safe = name.replace(/\W/g, "_");
|
|
354
|
+
let id = /^end$/i.test(safe) ? `${safe}_` : safe;
|
|
355
|
+
for (let n = 2; taken.has(id); n += 1) {
|
|
356
|
+
id = `${safe}_${n}`;
|
|
357
|
+
}
|
|
358
|
+
taken.add(id);
|
|
359
|
+
ids.set(name, id);
|
|
360
|
+
}
|
|
361
|
+
return ids;
|
|
362
|
+
}
|
|
363
|
+
function dependencyGraph(dependencies) {
|
|
364
|
+
const edges = [...dependencies].flatMap(
|
|
365
|
+
([from, { services }]) => services.map((dependency) => ({
|
|
366
|
+
from,
|
|
367
|
+
to: dependency.service,
|
|
368
|
+
label: kindsOf2(dependency).join(", ")
|
|
369
|
+
}))
|
|
370
|
+
);
|
|
371
|
+
if (edges.length === 0) {
|
|
372
|
+
return section("## Dependencies", ["No service's declarations name another service."]);
|
|
373
|
+
}
|
|
374
|
+
const names = [...new Set(edges.flatMap(({ from, to }) => [from, to]))].sort();
|
|
375
|
+
const ids = nodeIds(names);
|
|
376
|
+
const id = (name) => ids.get(name) ?? name;
|
|
377
|
+
const nodes = names.filter((name) => id(name) !== name).map((name) => ` ${id(name)}["${name.replace(/"/g, "#quot;")}"]`);
|
|
378
|
+
return section("## Dependencies", [
|
|
379
|
+
`An arrow points from a service to another its declarations name: ${code("inherit")} (its row policy), ${code("writes")} (that service's model), ${code("affects")}, ${code("scope")} (a ${code("{ scope, of }")} access form) or ${code("anchor")} (a collection's anchor). Each service's page lists them.`,
|
|
380
|
+
[
|
|
381
|
+
"```mermaid",
|
|
382
|
+
"graph LR",
|
|
383
|
+
...nodes,
|
|
384
|
+
...edges.map(({ from, to, label }) => ` ${id(from)} -->|${label}| ${id(to)}`),
|
|
385
|
+
"```"
|
|
386
|
+
]
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
|
|
223
390
|
// src/cli/schemaText.ts
|
|
224
391
|
var NO_JSON_SCHEMA = "unknown (no JSON Schema)";
|
|
225
392
|
var TARGET = "draft-07";
|
|
226
393
|
var MAX_DEPTH = 8;
|
|
227
394
|
var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
228
395
|
var ROOT = "#";
|
|
229
|
-
function
|
|
396
|
+
function isRecord3(value) {
|
|
230
397
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
231
398
|
}
|
|
232
399
|
function jsonSchemaOf(schema, mode) {
|
|
@@ -235,7 +402,7 @@ function jsonSchemaOf(schema, mode) {
|
|
|
235
402
|
}
|
|
236
403
|
try {
|
|
237
404
|
const json = schema["~standard"].jsonSchema[mode]({ target: TARGET });
|
|
238
|
-
return
|
|
405
|
+
return isRecord3(json) ? json : void 0;
|
|
239
406
|
} catch {
|
|
240
407
|
return void 0;
|
|
241
408
|
}
|
|
@@ -247,8 +414,8 @@ function definitionOf(root, ref) {
|
|
|
247
414
|
}
|
|
248
415
|
const [, group = "", name = ""] = match;
|
|
249
416
|
const definitions = root[group];
|
|
250
|
-
const found =
|
|
251
|
-
return
|
|
417
|
+
const found = isRecord3(definitions) ? definitions[decodeURIComponent(name)] : void 0;
|
|
418
|
+
return isRecord3(found) ? found : void 0;
|
|
252
419
|
}
|
|
253
420
|
function literal(value) {
|
|
254
421
|
return value === void 0 ? "undefined" : JSON.stringify(value);
|
|
@@ -287,13 +454,13 @@ function requiredOf(schema) {
|
|
|
287
454
|
return new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
|
|
288
455
|
}
|
|
289
456
|
function objectText(schema, scope) {
|
|
290
|
-
const properties =
|
|
457
|
+
const properties = isRecord3(schema.properties) ? schema.properties : {};
|
|
291
458
|
const required = requiredOf(schema);
|
|
292
459
|
const members = Object.entries(properties).map(
|
|
293
460
|
([name, value]) => `${propertyName(name, !required.has(name))}: ${textOf(value, scope)}`
|
|
294
461
|
);
|
|
295
462
|
const extra = schema.additionalProperties;
|
|
296
|
-
if (
|
|
463
|
+
if (isRecord3(extra)) {
|
|
297
464
|
const value = textOf(extra, scope);
|
|
298
465
|
if (members.length === 0) {
|
|
299
466
|
return `Record<string, ${value}>`;
|
|
@@ -351,10 +518,10 @@ function refText(ref, scope) {
|
|
|
351
518
|
return textOf(target, { ...scope, open: [...scope.open, ref] });
|
|
352
519
|
}
|
|
353
520
|
function textOf(schema, scope) {
|
|
354
|
-
if (schema === true ||
|
|
521
|
+
if (schema === true || isRecord3(schema) && Object.keys(schema).length === 0) {
|
|
355
522
|
return "unknown";
|
|
356
523
|
}
|
|
357
|
-
if (!
|
|
524
|
+
if (!isRecord3(schema) || scope.depth > MAX_DEPTH) {
|
|
358
525
|
return schema === false ? "never" : "unknown";
|
|
359
526
|
}
|
|
360
527
|
const inner = { ...scope, depth: scope.depth + 1 };
|
|
@@ -381,7 +548,7 @@ function textOf(schema, scope) {
|
|
|
381
548
|
if (typeof type === "string") {
|
|
382
549
|
return typeNameText(type, schema, inner);
|
|
383
550
|
}
|
|
384
|
-
return
|
|
551
|
+
return isRecord3(schema.properties) ? objectText(schema, inner) : "unknown";
|
|
385
552
|
}
|
|
386
553
|
function schemaText(schema) {
|
|
387
554
|
return textOf(schema, { root: schema, open: [ROOT], depth: 0 });
|
|
@@ -447,7 +614,7 @@ function schemaNotes(schema) {
|
|
|
447
614
|
}
|
|
448
615
|
function schemaFields(schema) {
|
|
449
616
|
const properties = schema.properties;
|
|
450
|
-
if (schema.type !== "object" || !
|
|
617
|
+
if (schema.type !== "object" || !isRecord3(properties) || Object.keys(properties).length === 0) {
|
|
451
618
|
return void 0;
|
|
452
619
|
}
|
|
453
620
|
const required = requiredOf(schema);
|
|
@@ -456,7 +623,7 @@ function schemaFields(schema) {
|
|
|
456
623
|
name,
|
|
457
624
|
optional: !required.has(name),
|
|
458
625
|
type: textOf(value, scope),
|
|
459
|
-
notes:
|
|
626
|
+
notes: isRecord3(value) ? schemaNotes(value) : ""
|
|
460
627
|
}));
|
|
461
628
|
}
|
|
462
629
|
|
|
@@ -547,6 +714,24 @@ function kindLine(method) {
|
|
|
547
714
|
}
|
|
548
715
|
return `A query that watches the ${code(method.watch.collection)} collection: a cached result is fetched again when the scope its input names changes.`;
|
|
549
716
|
}
|
|
717
|
+
function described(text) {
|
|
718
|
+
return text === void 0 ? [] : [paragraph(text)];
|
|
719
|
+
}
|
|
720
|
+
function callableBy(name, access) {
|
|
721
|
+
const method = access?.service?.methods.get(name);
|
|
722
|
+
if (method?.kinds === void 0) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const anonymous = method.access === "public" ? ", or anonymously" : "";
|
|
726
|
+
return [`- Callable by: ${list(method.kinds)}${anonymous}`];
|
|
727
|
+
}
|
|
728
|
+
function methodFacts(service, name, method, access) {
|
|
729
|
+
return [
|
|
730
|
+
`- MCP tool (default name): ${code(defaultToolName(service, name))}`,
|
|
731
|
+
...method.kind === "query" ? ["- MCP read-only hint: yes, as on every query's tool"] : [],
|
|
732
|
+
...callableBy(name, access)
|
|
733
|
+
];
|
|
734
|
+
}
|
|
550
735
|
function methodAccess(name, access) {
|
|
551
736
|
const method = access?.service?.methods.get(name);
|
|
552
737
|
if (method === void 0) {
|
|
@@ -557,11 +742,11 @@ function methodAccess(name, access) {
|
|
|
557
742
|
] : [];
|
|
558
743
|
return [`Access: ${accessFormText(method.access)}.`, ...rowless];
|
|
559
744
|
}
|
|
560
|
-
function methodSection(name, method, access) {
|
|
561
|
-
const described = method.describe === void 0 ? [] : [paragraph(method.describe)];
|
|
745
|
+
function methodSection(service, name, method, access) {
|
|
562
746
|
return section(`### ${code(name)}`, [
|
|
563
|
-
...described,
|
|
747
|
+
...described(method.describe),
|
|
564
748
|
kindLine(method),
|
|
749
|
+
methodFacts(service, name, method, access),
|
|
565
750
|
...methodAccess(name, access),
|
|
566
751
|
...inputBlocks(method),
|
|
567
752
|
`Output: ${outputText(method.output)}.`
|
|
@@ -575,7 +760,7 @@ function methodsSection(contract, access) {
|
|
|
575
760
|
return [
|
|
576
761
|
"## Methods",
|
|
577
762
|
"",
|
|
578
|
-
...entries.flatMap(([name, method]) => methodSection(name, method, access))
|
|
763
|
+
...entries.flatMap(([name, method]) => methodSection(contract.name, name, method, access))
|
|
579
764
|
];
|
|
580
765
|
}
|
|
581
766
|
function scopeText(scope) {
|
|
@@ -603,6 +788,7 @@ function collectionSection(name, collection, access) {
|
|
|
603
788
|
const maxLimit = collection.maxLimit ?? DEFAULT_COLLECTION_MAX_LIMIT;
|
|
604
789
|
const served = access?.service?.collections.get(name);
|
|
605
790
|
return section(`### ${code(name)}`, [
|
|
791
|
+
...described(collection.describe),
|
|
606
792
|
optionTable([
|
|
607
793
|
["Scope", scopeText(collection.scope)],
|
|
608
794
|
["Item", code(collection.item)],
|
|
@@ -673,6 +859,7 @@ function streamSection(name, stream, access) {
|
|
|
673
859
|
const scoped = isScopedStream(stream);
|
|
674
860
|
const served = access?.service?.streams.get(name);
|
|
675
861
|
return section(`### ${code(name)}`, [
|
|
862
|
+
...described(stream.describe),
|
|
676
863
|
optionTable([
|
|
677
864
|
["Item", code(typeOf(stream.item, "output"))],
|
|
678
865
|
["Scope", scoped ? `one feed per ${code(stream.scope ?? "")}` : "one global feed"],
|
|
@@ -700,6 +887,7 @@ function channelSection(name, channel, access) {
|
|
|
700
887
|
const rate = channel.ratePerSecond ?? CHANNEL_DEFAULT_RATE;
|
|
701
888
|
const served = access?.service?.channels.has(name) === true;
|
|
702
889
|
return section(`### ${code(name)}`, [
|
|
890
|
+
...described(channel.describe),
|
|
703
891
|
optionTable([
|
|
704
892
|
["Payload", code(typeOf(channel.payload, "input"))],
|
|
705
893
|
[
|
|
@@ -717,6 +905,7 @@ function channelSection(name, channel, access) {
|
|
|
717
905
|
}
|
|
718
906
|
function eventSection(name, event) {
|
|
719
907
|
return section(`### ${code(name)}`, [
|
|
908
|
+
...described(event.describe),
|
|
720
909
|
optionTable([["Payload", code(typeOf(event.payload, "output"))]])
|
|
721
910
|
]);
|
|
722
911
|
}
|
|
@@ -745,6 +934,10 @@ function accessSection(contract, access) {
|
|
|
745
934
|
"Service-wide Admin",
|
|
746
935
|
service.adminBypass ? "passes every check of the service" : `passes only the forms that name ${code("service")} (${code("adminBypass: false")})`
|
|
747
936
|
],
|
|
937
|
+
[
|
|
938
|
+
"Principal kinds",
|
|
939
|
+
service.kinds === void 0 ? void 0 : `${list(service.kinds)}: its methods, subscriptions and channels refuse every other kind`
|
|
940
|
+
],
|
|
748
941
|
["Change topic", watchAccessText(service.watchAccess)],
|
|
749
942
|
["Field levels", fieldLevelsText(contract)]
|
|
750
943
|
]),
|
|
@@ -757,7 +950,9 @@ function renderService(contract, access) {
|
|
|
757
950
|
"",
|
|
758
951
|
`# ${contract.name}`,
|
|
759
952
|
"",
|
|
953
|
+
...described(contract.describe).flatMap((text) => [text, ""]),
|
|
760
954
|
...accessSection(contract, access),
|
|
955
|
+
...access?.dependencies === void 0 ? [] : dependsOnSection(access.dependencies),
|
|
761
956
|
...entitySection(contract),
|
|
762
957
|
...projectionsSection(contract),
|
|
763
958
|
...methodsSection(contract, access),
|
|
@@ -787,10 +982,12 @@ function renderService(contract, access) {
|
|
|
787
982
|
function fileOf(contract) {
|
|
788
983
|
return `${contract.name.replace(/[^\w.-]/g, "-")}.md`;
|
|
789
984
|
}
|
|
790
|
-
function renderIndex(contracts) {
|
|
985
|
+
function renderIndex(contracts, dependencies) {
|
|
791
986
|
const count = (members) => String(Object.keys(members).length);
|
|
987
|
+
const hasDescribe = contracts.some((contract) => contract.describe !== void 0);
|
|
792
988
|
const rows = contracts.map((contract) => [
|
|
793
989
|
`[${contract.name}](${fileOf(contract)})`,
|
|
990
|
+
...hasDescribe ? [contract.describe ?? ""] : [],
|
|
794
991
|
count(contract.methods),
|
|
795
992
|
count(contract.collections),
|
|
796
993
|
count(contract.streams),
|
|
@@ -804,9 +1001,21 @@ function renderIndex(contracts) {
|
|
|
804
1001
|
"",
|
|
805
1002
|
"One page per service, generated from its contract by `quickdraw-docs`.",
|
|
806
1003
|
"",
|
|
807
|
-
...table(
|
|
1004
|
+
...table(
|
|
1005
|
+
[
|
|
1006
|
+
"Service",
|
|
1007
|
+
...hasDescribe ? ["Description"] : [],
|
|
1008
|
+
"Methods",
|
|
1009
|
+
"Collections",
|
|
1010
|
+
"Streams",
|
|
1011
|
+
"Channels",
|
|
1012
|
+
"Events"
|
|
1013
|
+
],
|
|
1014
|
+
rows
|
|
1015
|
+
),
|
|
1016
|
+
...dependencies === void 0 ? [] : ["", ...dependencyGraph(dependencies)]
|
|
808
1017
|
];
|
|
809
|
-
return `${lines.join("\n")}
|
|
1018
|
+
return `${lines.join("\n").trimEnd()}
|
|
810
1019
|
`;
|
|
811
1020
|
}
|
|
812
1021
|
|
|
@@ -815,7 +1024,8 @@ var USAGE = `Usage: quickdraw-docs <module> [--services <module>] [--out <dir>]
|
|
|
815
1024
|
|
|
816
1025
|
<module> a module exporting the app's contracts: each one, or a map of them
|
|
817
1026
|
--services <module> a module exporting the services (each, or a list of them): the pages
|
|
818
|
-
then also say who may call each method, from the services' access
|
|
1027
|
+
then also say who may call each method, from the services' access,
|
|
1028
|
+
and which services each depends on, with a graph on the index
|
|
819
1029
|
--out <dir> where the pages go: one per service, plus README.md (default: docs/api)
|
|
820
1030
|
--check write nothing; exit 1 when the pages on disk differ from the contracts
|
|
821
1031
|
`;
|
|
@@ -829,11 +1039,11 @@ var CONTRACT_KEYS = [
|
|
|
829
1039
|
"channels",
|
|
830
1040
|
"events"
|
|
831
1041
|
];
|
|
832
|
-
function
|
|
1042
|
+
function isRecord4(value) {
|
|
833
1043
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
834
1044
|
}
|
|
835
1045
|
function isContract(value) {
|
|
836
|
-
return
|
|
1046
|
+
return isRecord4(value) && Object.isFrozen(value) && typeof value.name === "string" && "entity" in value && CONTRACT_KEYS.every((key) => isRecord4(value[key]));
|
|
837
1047
|
}
|
|
838
1048
|
function contractsOf(exports) {
|
|
839
1049
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -847,7 +1057,7 @@ function contractsOf(exports) {
|
|
|
847
1057
|
for (const value of Object.values(exports)) {
|
|
848
1058
|
if (isContract(value)) {
|
|
849
1059
|
add(value);
|
|
850
|
-
} else if (
|
|
1060
|
+
} else if (isRecord4(value)) {
|
|
851
1061
|
Object.values(value).filter(isContract).forEach(add);
|
|
852
1062
|
}
|
|
853
1063
|
}
|
|
@@ -864,6 +1074,7 @@ function generateDocs(contracts, options = {}) {
|
|
|
864
1074
|
);
|
|
865
1075
|
}
|
|
866
1076
|
}
|
|
1077
|
+
const dependencies = services === void 0 ? void 0 : dependenciesOf(contracts, services);
|
|
867
1078
|
const files = /* @__PURE__ */ new Map();
|
|
868
1079
|
for (const contract of contracts) {
|
|
869
1080
|
const file = fileOf(contract);
|
|
@@ -874,11 +1085,14 @@ function generateDocs(contracts, options = {}) {
|
|
|
874
1085
|
file,
|
|
875
1086
|
renderService(
|
|
876
1087
|
contract,
|
|
877
|
-
services === void 0 ? void 0 : {
|
|
1088
|
+
services === void 0 ? void 0 : {
|
|
1089
|
+
service: services.get(contract.name),
|
|
1090
|
+
dependencies: dependencies?.get(contract.name)
|
|
1091
|
+
}
|
|
878
1092
|
)
|
|
879
1093
|
);
|
|
880
1094
|
}
|
|
881
|
-
files.set(INDEX_FILE, renderIndex(contracts));
|
|
1095
|
+
files.set(INDEX_FILE, renderIndex(contracts, dependencies));
|
|
882
1096
|
return files;
|
|
883
1097
|
}
|
|
884
1098
|
function readText(path) {
|