lazypock 0.8.4 → 0.10.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/README.md +72 -14
- package/dist/{chunk-CGQVJ7TT.js → chunk-JYKRCEC3.js} +17 -16
- package/dist/{chunk-CGQVJ7TT.js.map → chunk-JYKRCEC3.js.map} +1 -1
- package/dist/cli.cjs +16 -15
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +16 -15
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +135 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -18
- package/dist/index.d.ts +108 -18
- package/dist/index.global.js +135 -46
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +120 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +16 -15
- package/src/collection.ts +77 -9
- package/src/http.ts +20 -1
- package/src/index.ts +1 -1
- package/src/lazypock.ts +40 -1
- package/src/realtime.ts +104 -11
- package/src/types.ts +37 -10
package/dist/cli.global.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["#!/usr/bin/env node\n// ── Codegen CLI ─────────────────────────────────────────\n// `lazypock` — fetches the live collection schema from a Lazypock\n// API and writes a fully-typed `lazypock.types.ts` module.\n//\n// Auth methods (pick one):\n// 1. Superuser email + password:\n// npx lazypock --url http://localhost:4000/api --email admin@... --password ...\n// 2. API key (recommended, generated from the Settings dashboard):\n// npx lazypock --url http://localhost:4000/api --apikey <key>\n//\n// Or via env vars (no flags):\n// LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock\n// LAZYPOCK_URL=... LAZYPOCK_EMAIL=... LAZYPOCK_PASSWORD=... npx lazypock\n//\n// NOTE: `lazypock-gen` remains as a deprecated alias for backwards\n// compatibility. Both invoke the same executable.\n\nimport { writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { generateTypes } from \"./codegen\";\nimport type { CollectionsResponse } from \"./schema\";\n\ninterface CliOptions {\n\turl: string;\n\temail: string;\n\tpassword: string;\n\tapiKey: string;\n\tout: string;\n\tpackageName: string;\n\tskipSystem: boolean;\n}\n\nfunction fail(msg: string): never {\n\tconsole.error(`\\n❌ ${msg}\\n`);\n\tprocess.exit(1);\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n\tconst args = [...argv];\n\tconst get = (flag: string, envKey: string, def = \"\"): string => {\n\t\tconst i = args.indexOf(flag);\n\t\tif (i !== -1 && i + 1 < args.length) return args[i + 1];\n\t\treturn process.env[envKey] ?? def;\n\t};\n\tconst has = (flag: string): boolean => args.includes(flag);\n\n\tconst url = get(\"--url\", \"LAZYPOCK_URL\");\n\tconst email = get(\"--email\", \"LAZYPOCK_EMAIL\");\n\tconst password = get(\"--password\", \"LAZYPOCK_PASSWORD\");\n\tconst apiKey =\n\t\tget(\"--apikey\", \"LAZYPOCK_API_KEY\") || get(\"--api-key\", \"LAZYPOCK_API_KEY\");\n\n\tif (!url) fail(\"Missing API URL. Pass --url or set LAZYPOCK_URL.\");\n\tif (!apiKey) {\n\t\tif (!email)\n\t\t\tfail(\n\t\t\t\t\"Missing credentials. Pass --apikey, or --email + --password, or set LAZYPOCK_API_KEY / LAZYPOCK_EMAIL.\",\n\t\t\t);\n\t\tif (!password)\n\t\t\tfail(\"Missing password. Pass --password, or set LAZYPOCK_PASSWORD.\");\n\t}\n\n\tconst out =\n\t\tget(\"--output\", \"LAZYPOCK_OUT\") ||\n\t\tget(\"--out\", \"LAZYPOCK_OUT\", \"lazypock.types.ts\");\n\tconst packageName = get(\"--package\", \"LAZYPOCK_PACKAGE\", \"lazypock\");\n\n\treturn {\n\t\turl,\n\t\temail,\n\t\tpassword,\n\t\tapiKey,\n\t\tout,\n\t\tpackageName,\n\t\tskipSystem: has(\"--skip-system\"),\n\t};\n}\n\nasync function fetchCollections(\n\topts: CliOptions,\n): Promise<CollectionsResponse> {\n\tconst base = opts.url.replace(/\\/+$/, \"\");\n\n\tlet authKey: string;\n\tif (opts.apiKey) {\n\t\t// Step 1: use a stored API key directly (no login round-trip).\n\t\t// The key is sent as `Authorization: Bearer <key>` and recognised\n\t\t// by the backend's Auth.Plug as an API key.\n\t\tauthKey = opts.apiKey;\n\t} else {\n\t\t// Step 1: login as superuser to get a token.\n\t\t// Prefer the PocketBase-parity `_superusers` auth collection endpoint,\n\t\t// fall back to the legacy /superusers/login for older servers.\n\t\tlet loginRes = await fetch(base + \"/_superusers/auth-with-password\", {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({ identity: opts.email, password: opts.password }),\n\t\t});\n\t\tif (!loginRes.ok) {\n\t\t\tloginRes = await fetch(base + \"/superusers/login\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ email: opts.email, password: opts.password }),\n\t\t\t});\n\t\t}\n\t\tif (!loginRes.ok) {\n\t\t\tconst text = await loginRes.text();\n\t\t\tfail(\n\t\t\t\t`Superuser login failed (${loginRes.status}). ${text.slice(0, 200)}`,\n\t\t\t);\n\t\t}\n\t\tconst loginData = (await loginRes.json()) as { token?: string };\n\t\tif (!loginData.token) fail(\"Login response did not include a token.\");\n\t\tauthKey = loginData.token;\n\t}\n\n\t// Step 2: fetch collections\n\tconst collRes = await fetch(base + \"/collections\", {\n\t\theaders: { Authorization: \"Bearer \" + authKey },\n\t});\n\tif (!collRes.ok) {\n\t\tconst text = await collRes.text();\n\t\tfail(\n\t\t\t`Failed to fetch collections (${collRes.status}). ${text.slice(0, 200)}`,\n\t\t);\n\t}\n\treturn (await collRes.json()) as CollectionsResponse;\n}\n\nasync function main(): Promise<void> {\n\tconst opts = parseArgs(process.argv.slice(2));\n\tconst authLabel = opts.apiKey\n\t\t? `API key ${opts.apiKey.slice(0, 4)}…${opts.apiKey.slice(-4)}`\n\t\t: opts.email;\n\tconsole.log(`\\n🔌 Connecting to ${opts.url} as ${authLabel} …`);\n\n\tconst { items } = await fetchCollections(opts);\n\tconsole.log(`📦 Found ${items.length} collection(s).`);\n\n\tconst source = generateTypes(items, {\n\t\tpackageName: opts.packageName,\n\t\tskipSystem: opts.skipSystem,\n\t});\n\n\tconst outPath = resolve(process.cwd(), opts.out);\n\tawait writeFile(outPath, source, \"utf8\");\n\tconsole.log(`✅ Wrote ${outPath} (${source.length} bytes).`);\n\tconsole.log(\n\t\t`\\nImport it in your app:\\n import { createClient } from './${opts.out.replace(/\\.ts$/, \"\")}';\\n`,\n\t);\n}\n\nmain().catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n","// ── Type mapping (runtime + codegen) ────────────────────\n// Maps server field types to TypeScript types.\n// Shared by the runtime `SchemaTypes` helper and the codegen CLI.\n\nimport type { SchemaField } from \"./schema\";\n\n/**\n * Map a single server field to its TypeScript type string.\n * Used by the codegen CLI to emit interface members.\n *\n * @param field The field definition.\n * @param fallback Fallback type for unknown field types (default `unknown`).\n */\nexport function fieldTypeScriptType(\n\tfield: SchemaField,\n\tfallback = \"unknown\",\n): string {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn values.map((v) => JSON.stringify(String(v))).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\t\t}\n\t\tcase \"multi_select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn `(${values.map((v) => JSON.stringify(String(v))).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\t\t}\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"multi_file\":\n\t\t\treturn \"string[]\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"Record<string, unknown>\";\n\t\tcase \"relation\":\n\t\t\t// Relations store the target record's ID (string) — or an array\n\t\t\t// of IDs when multi-relation (maxSelect > 1).\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"string[]\" : \"string\";\n\t\tcase \"password\":\n\t\t\t// Passwords are write-only; never expose on read models.\n\t\t\treturn \"never\";\n\t\tdefault:\n\t\t\treturn fallback;\n\t}\n}\n\n/**\n * Returns the runtime type kind for a field — used by {@link schemaFieldType}\n * to build structural types at runtime.\n */\nexport type FieldTypeKind =\n\t| \"string\"\n\t| \"number\"\n\t| \"boolean\"\n\t| \"string-array\"\n\t| \"json\"\n\t| \"relation\"\n\t| \"relation-many\"\n\t| \"password\"\n\t| \"unknown\";\n\n/** Map a server field to its runtime type kind. */\nexport function fieldTypeKind(field: SchemaField): FieldTypeKind {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\tcase \"select\":\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"multi_select\":\n\t\tcase \"multi_file\":\n\t\t\treturn \"string-array\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"json\";\n\t\tcase \"relation\":\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"relation-many\" : \"relation\";\n\t\tcase \"password\":\n\t\t\treturn \"password\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Derive a TypeScript field type from a {@link SchemaField} — the runtime\n * counterpart to the codegen mapper. Lets consumers build typed clients\n * from a fetched schema without running the CLI.\n */\nexport function schemaFieldType(field: SchemaField): unknown {\n\tswitch (fieldTypeKind(field)) {\n\t\tcase \"string\":\n\t\t\treturn String;\n\t\tcase \"number\":\n\t\t\treturn Number;\n\t\tcase \"boolean\":\n\t\t\treturn Boolean;\n\t\tcase \"string-array\":\n\t\t\treturn [String] as const;\n\t\tcase \"relation\":\n\t\t\treturn String;\n\t\tcase \"relation-many\":\n\t\t\treturn [String] as const;\n\t\tcase \"json\":\n\t\t\treturn Object;\n\t\tcase \"password\":\n\t\t\treturn undefined;\n\t\tcase \"unknown\":\n\t\t\treturn undefined;\n\t}\n}\n","// ── Codegen ─────────────────────────────────────────────\n// Generates a `lazypock.types.ts` module from the live API schema.\n//\n// The generated file exports:\n// - One interface per collection (e.g. `PostsRecord`)\n// - A `LazypockCollections` map: { posts: PostsRecord; users: UsersRecord; ... }\n// - A `createClient()` factory pre-bound to those types, so\n// `client.collection(\"posts\").create({ title: \"x\" })` is fully type-checked.\n//\n// The CLI in `src/cli.ts` wires this to `GET /collections`.\n\nimport type { CollectionSchema, SchemaField } from \"./schema\";\nimport { fieldTypeScriptType } from \"./typegen\";\n\n/** Format a raw collection name into a valid TS identifier (PascalCase). */\nexport function collectionTypeName(name: string): string {\n\treturn name\n\t\t.split(/[^a-zA-Z0-9]+/)\n\t\t.filter(Boolean)\n\t\t.map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n\t\t.join(\"\")\n\t\t.replace(/^[0-9]/, \"_$&\");\n}\n\n/** Interface member name — sanitize hyphens/spaces but keep readability. */\nexport function fieldKey(name: string): string {\n\treturn name.replace(/[^a-zA-Z0-9_]/g, \"_\");\n}\n\n\n\n/**\n * Generate the full TypeScript source for the typed SDK module.\n *\n * @param collections Collections fetched from the API.\n * @param options Generation options.\n */\nexport function generateTypes(\n\tcollections: CollectionSchema[],\n\toptions: {\n\t\t/** Import specifier for the lazypock package (default `lazypock`). */\n\t\tpackageName?: string;\n\t\t/** Emit base record fields (id, created, updated, …). Default true. */\n\t\tincludeBaseFields?: boolean;\n\t\t/** Skip system collections (names starting with `_` or `users`). Default false. */\n\t\tskipSystem?: boolean;\n\t} = {},\n): string {\n\tconst {\n\t\tpackageName = \"lazypock\",\n\t\tincludeBaseFields = true,\n\t\tskipSystem = false,\n\t} = options;\n\n\tconst filtered = skipSystem\n\t\t? collections.filter(\n\t\t\t\t(c) => !c.system && !c.name.startsWith(\"_\") && c.name !== \"users\",\n\t\t\t)\n\t\t: collections;\n\n\tconst sections: string[] = [];\n\tsections.push(`// ── Auto-generated by lazypock-ts ──────────────────────────\n// Do not edit by hand. Regenerate with: npx lazypock-gen\n// Schema snapshot: ${new Date().toISOString()}`);\n\n\tif (includeBaseFields) {\n\t\tsections.push(`export interface BaseRecord {\n id: string;\n collectionId: string;\n collectionName: string;\n created: string;\n updated: string;\n}`);\n\t}\n\n\t// One interface per collection\n\tfor (const coll of filtered) {\n\t\tconst typeName = collectionTypeName(coll.name);\n\t\tconst fields = coll.fields ?? [];\n\t\tconst lines = fields.map((f) => memberLine(f)).filter((l) => l !== \"\");\n\t\tconst body = lines.join(\"\\n\");\n\t\tsections.push(\n\t\t\t`export interface ${typeName}Record${renderInterface({\n\t\t\t\textends: includeBaseFields ? \"BaseRecord\" : undefined,\n\t\t\t\tbody,\n\t\t\t})}`,\n\t\t);\n\n\t\t// Write-only create data: what `create()`/`update()` accept. This\n\t\t// intentionally includes password fields (and hidden fields) that are\n\t\t// excluded from the read model — the server accepts them on write,\n\t\t// hashes passwords, and never returns them in responses.\n\t\tconst createLines: string[] = [];\n\t\tconst seenKeys = new Set<string>();\n\t\tfor (const f of fields) {\n\t\t\tfor (const { key, line } of createDataMemberLines(f)) {\n\t\t\t\tif (seenKeys.has(key)) continue;\n\t\t\t\tseenKeys.add(key);\n\t\t\t\tcreateLines.push(line);\n\t\t\t}\n\t\t}\n\t\tsections.push(\n\t\t\t`export interface ${typeName}CreateData${renderInterface({\n\t\t\t\tbody: createLines.join(\"\\n\"),\n\t\t\t})}`,\n\t\t);\n\t}\n\n\t// Auth collection type\n\tsections.push(`export interface AuthRecord extends BaseRecord {\n email: string;\n verified: boolean;\n}`);\n\n\t// Collections map\n\tconst mapEntries = filtered\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` \"${c.name}\": ${collectionTypeName(c.name)}Record${\n\t\t\t\t\tc.type === \"auth\" ? \" & AuthRecord\" : \"\"\n\t\t\t\t};`,\n\t\t)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCollections {\n${mapEntries}\n}`);\n\n\t// Create-data map — typed `create()`/`update()` payloads, including\n\t// write-only password fields.\n\tconst createMapEntries = filtered\n\t\t.map((c) => ` \"${c.name}\": ${collectionTypeName(c.name)}CreateData;`)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCreateData {\n${createMapEntries}\n}`);\n\n\t// Runtime schema snapshot — lets the client exclude hidden fields by\n\t// default, validate select/expand, and derive visible field lists.\n\tconst schemaEntries = collections\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` {\n id: ${JSON.stringify(c.id ?? null)},\n name: ${JSON.stringify(c.name)},\n type: ${JSON.stringify(c.type)},\n system: ${JSON.stringify(c.system ?? false)},\n fields: ${JSON.stringify(c.fields ?? [], null, 2)},\n rules: ${JSON.stringify(c.rules ?? {})},\n options: ${JSON.stringify(c.options ?? {})},\n }`,\n\t\t)\n\t\t.join(\",\\n\");\n\tsections.push(`// Schema snapshot (runtime) — hidden-field exclusion + query validation.\nexport const lazypockSchema: import(\"${packageName}\").CollectionSchema[] = [\n${schemaEntries}\n];`);\n\n\t// createClient factory\n\tsections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from \"${packageName}\";\n\n/**\n * Create a Lazypock client typed against this schema snapshot.\n * Collection access is fully type-checked:\n * client.collection(\"posts\").create({ title: \"x\" }) // title must exist\n *\n * For auth collections the generated *CreateData type includes the\n * write-only password field, so creating a user is type-safe:\n * client.collection(\"users\").create({ email, password }) // ✓\n *\n * The schema snapshot is wired into the client automatically, so hidden\n * fields are excluded from responses and select/expand are validated.\n */\nexport function createClient(options: LazypockClientOptions): TypedClient {\n return new TypedClient({\n ...options,\n types: {\n ...options.types,\n schemas: options.types?.schemas ?? lazypockSchema,\n },\n });\n}\n\nexport class TypedClient extends LazypockClient {\n // K extends keyof LazypockCollections (rather than a generic string with a\n // conditional return) so the IDE suggests collection names and unknown names\n // are rejected at compile time:\n // client.collection(\"posts\") // suggested + typed\n // client.collection(\"nope\") // TS error\n //\n // create()/update() take the collection's *CreateData — the read model\n // omits password/hidden fields, but the write model carries them.\n // T extends string (rather than keyof LazypockCollections) with a conditional\n // return type, so the IDE suggests collection names AND unknown/dynamic names\n // still resolve to the untyped service — a studio that manages\n // user-created collections must be able to call collection(someString).\n //\n // create()/update() take the collection's *CreateData — the read model\n // omits password/hidden fields, but the write model carries them.\n override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>\n : CollectionService<unknown>;\n }\n}\n`);\n\n\treturn sections.join(\"\\n\\n\") + \"\\n\";\n}\n\n/**\n * Render the interface body including the extends clause.\n * Empty body → ` extends BaseRecord {}` (valid TS).\n */\nfunction renderInterface(opts: { extends?: string; body: string }): string {\n\tconst ext = opts.extends ? ` extends ${opts.extends}` : \"\";\n\tif (!opts.body) return `${ext} {}`;\n\treturn `${ext} {\\n${opts.body}\\n}`;\n}\n\n/** Render a single interface member line for a field. */\nfunction memberLine(f: SchemaField): string {\n\t// Hidden fields are excluded from API responses by the schema-aware\n\t// client default — don't expose them on the read model either.\n\tif (f.hidden) return \"\";\n\tconst key = fieldKey(f.name);\n\tconst req = f.required || f.type === \"password\" ? \"\" : \"?\";\n\tconst type = fieldTypeScriptType(f);\n\t// `never` members (passwords) are omitted from read models.\n\tif (type === \"never\") return \"\";\n\treturn ` ${JSON.stringify(key)}${req}: ${type};`;\n}\n\n/**\n * Render the write-only create-data members for a field.\n *\n * Unlike the read model, create/update data includes password fields (sent as\n * plain strings — the server hashes them) and hidden fields: the server\n * accepts them on write and never returns them in responses.\n *\n * Password fields are exposed under the canonical PocketBase API key\n * `password` (the server accepts `password` for any single password field and\n * hashes it into the backing column, e.g. `password_hash`), with the raw\n * metadata name kept as a backward-compat alias.\n *\n * A field is only marked required when it actually needs a client value:\n * fields with a server-side default (`options.defaultValue`) and the auth\n * system fields `verified` / `emailVisibility` (the server defaults them to\n * false/true even though the snapshot doesn't carry `defaultValue`) stay\n * optional, so `create({ email, password })` typechecks without forcing\n * callers to send values the server would fill in anyway. Password fields\n * are optional (accounts may exist without a password, e.g. OAuth-only).\n */\nfunction createDataMemberLines(\n\tf: SchemaField,\n): { key: string; line: string }[] {\n\t// Autodate columns are server-managed; never writable.\n\tif (f.type === \"autodate\") return [];\n\tconst rawName = fieldKey(f.name);\n\tconst serverDefaulted =\n\t\tf.options?.defaultValue !== undefined ||\n\t\t(f.system && (f.name === \"verified\" || f.name === \"emailVisibility\"));\n\tconst req = f.required && !serverDefaulted ? \"\" : \"?\";\n\tconst type = f.type === \"password\" ? \"string\" : fieldTypeScriptType(f);\n\tif (type === \"never\") return [];\n\n\tconst members = [\n\t\t{ key: rawName, line: ` ${JSON.stringify(rawName)}${req}: ${type};` },\n\t];\n\t// Write-only password: canonical key is `password` (PocketBase parity) —\n\t// the server accepts `password` for any single password field and hashes\n\t// it. The raw metadata name (e.g. `password_hash`) stays as a compat alias.\n\tif (f.type === \"password\" && rawName !== \"password\") {\n\t\tmembers.unshift({\n\t\t\tkey: \"password\",\n\t\t\tline: ` ${JSON.stringify(\"password\")}${req}: ${type};`,\n\t\t});\n\t}\n\treturn members;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["#!/usr/bin/env node\n// ── Codegen CLI ─────────────────────────────────────────\n// `lazypock` — fetches the live collection schema from a Lazypock\n// API and writes a fully-typed `lazypock.types.ts` module.\n//\n// Auth methods (pick one):\n// 1. Superuser email + password:\n// npx lazypock --url http://localhost:4000/api --email admin@... --password ...\n// 2. API key (recommended, generated from the Settings dashboard):\n// npx lazypock --url http://localhost:4000/api --apikey <key>\n//\n// Or via env vars (no flags):\n// LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock\n// LAZYPOCK_URL=... LAZYPOCK_EMAIL=... LAZYPOCK_PASSWORD=... npx lazypock\n//\n// NOTE: `lazypock-gen` remains as a deprecated alias for backwards\n// compatibility. Both invoke the same executable.\n\nimport { writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { generateTypes } from \"./codegen\";\nimport type { CollectionsResponse } from \"./schema\";\n\ninterface CliOptions {\n\turl: string;\n\temail: string;\n\tpassword: string;\n\tapiKey: string;\n\tout: string;\n\tpackageName: string;\n\tskipSystem: boolean;\n}\n\nfunction fail(msg: string): never {\n\tconsole.error(`\\n❌ ${msg}\\n`);\n\tprocess.exit(1);\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n\tconst args = [...argv];\n\tconst get = (flag: string, envKey: string, def = \"\"): string => {\n\t\tconst i = args.indexOf(flag);\n\t\tif (i !== -1 && i + 1 < args.length) return args[i + 1];\n\t\treturn process.env[envKey] ?? def;\n\t};\n\tconst has = (flag: string): boolean => args.includes(flag);\n\n\tconst url = get(\"--url\", \"LAZYPOCK_URL\");\n\tconst email = get(\"--email\", \"LAZYPOCK_EMAIL\");\n\tconst password = get(\"--password\", \"LAZYPOCK_PASSWORD\");\n\tconst apiKey =\n\t\tget(\"--apikey\", \"LAZYPOCK_API_KEY\") || get(\"--api-key\", \"LAZYPOCK_API_KEY\");\n\n\tif (!url) fail(\"Missing API URL. Pass --url or set LAZYPOCK_URL.\");\n\tif (!apiKey) {\n\t\tif (!email)\n\t\t\tfail(\n\t\t\t\t\"Missing credentials. Pass --apikey, or --email + --password, or set LAZYPOCK_API_KEY / LAZYPOCK_EMAIL.\",\n\t\t\t);\n\t\tif (!password)\n\t\t\tfail(\"Missing password. Pass --password, or set LAZYPOCK_PASSWORD.\");\n\t}\n\n\tconst out =\n\t\tget(\"--output\", \"LAZYPOCK_OUT\") ||\n\t\tget(\"--out\", \"LAZYPOCK_OUT\", \"lazypock.types.ts\");\n\tconst packageName = get(\"--package\", \"LAZYPOCK_PACKAGE\", \"lazypock\");\n\n\treturn {\n\t\turl,\n\t\temail,\n\t\tpassword,\n\t\tapiKey,\n\t\tout,\n\t\tpackageName,\n\t\tskipSystem: has(\"--skip-system\"),\n\t};\n}\n\nasync function fetchCollections(\n\topts: CliOptions,\n): Promise<CollectionsResponse> {\n\tconst base = opts.url.replace(/\\/+$/, \"\");\n\n\tlet authKey: string;\n\tif (opts.apiKey) {\n\t\t// Step 1: use a stored API key directly (no login round-trip).\n\t\t// The key is sent as `Authorization: Bearer <key>` and recognised\n\t\t// by the backend's Auth.Plug as an API key.\n\t\tauthKey = opts.apiKey;\n\t} else {\n\t\t// Step 1: login as superuser to get a token.\n\t\t// Prefer the PocketBase-parity `_superusers` auth collection endpoint,\n\t\t// fall back to the legacy /superusers/login for older servers.\n\t\tlet loginRes = await fetch(base + \"/_superusers/auth-with-password\", {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({ identity: opts.email, password: opts.password }),\n\t\t});\n\t\tif (!loginRes.ok) {\n\t\t\tloginRes = await fetch(base + \"/superusers/login\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ email: opts.email, password: opts.password }),\n\t\t\t});\n\t\t}\n\t\tif (!loginRes.ok) {\n\t\t\tconst text = await loginRes.text();\n\t\t\tfail(\n\t\t\t\t`Superuser login failed (${loginRes.status}). ${text.slice(0, 200)}`,\n\t\t\t);\n\t\t}\n\t\tconst loginData = (await loginRes.json()) as { token?: string };\n\t\tif (!loginData.token) fail(\"Login response did not include a token.\");\n\t\tauthKey = loginData.token;\n\t}\n\n\t// Step 2: fetch collections\n\tconst collRes = await fetch(base + \"/collections\", {\n\t\theaders: { Authorization: \"Bearer \" + authKey },\n\t});\n\tif (!collRes.ok) {\n\t\tconst text = await collRes.text();\n\t\tfail(\n\t\t\t`Failed to fetch collections (${collRes.status}). ${text.slice(0, 200)}`,\n\t\t);\n\t}\n\treturn (await collRes.json()) as CollectionsResponse;\n}\n\nasync function main(): Promise<void> {\n\tconst opts = parseArgs(process.argv.slice(2));\n\tconst authLabel = opts.apiKey\n\t\t? `API key ${opts.apiKey.slice(0, 4)}…${opts.apiKey.slice(-4)}`\n\t\t: opts.email;\n\tconsole.log(`\\n🔌 Connecting to ${opts.url} as ${authLabel} …`);\n\n\tconst { items } = await fetchCollections(opts);\n\tconsole.log(`📦 Found ${items.length} collection(s).`);\n\n\tconst source = generateTypes(items, {\n\t\tpackageName: opts.packageName,\n\t\tskipSystem: opts.skipSystem,\n\t});\n\n\tconst outPath = resolve(process.cwd(), opts.out);\n\tawait writeFile(outPath, source, \"utf8\");\n\tconsole.log(`✅ Wrote ${outPath} (${source.length} bytes).`);\n\tconsole.log(\n\t\t`\\nImport it in your app:\\n import { createClient } from './${opts.out.replace(/\\.ts$/, \"\")}';\\n`,\n\t);\n}\n\nmain().catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n","// ── Type mapping (runtime + codegen) ────────────────────\n// Maps server field types to TypeScript types.\n// Shared by the runtime `SchemaTypes` helper and the codegen CLI.\n\nimport type { SchemaField } from \"./schema\";\n\n/**\n * Map a single server field to its TypeScript type string.\n * Used by the codegen CLI to emit interface members.\n *\n * @param field The field definition.\n * @param fallback Fallback type for unknown field types (default `unknown`).\n */\nexport function fieldTypeScriptType(\n\tfield: SchemaField,\n\tfallback = \"unknown\",\n): string {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn values.map((v) => JSON.stringify(String(v))).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\t\t}\n\t\tcase \"multi_select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn `(${values.map((v) => JSON.stringify(String(v))).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\t\t}\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"multi_file\":\n\t\t\treturn \"string[]\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"Record<string, unknown>\";\n\t\tcase \"relation\":\n\t\t\t// Relations store the target record's ID (string) — or an array\n\t\t\t// of IDs when multi-relation (maxSelect > 1).\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"string[]\" : \"string\";\n\t\tcase \"password\":\n\t\t\t// Passwords are write-only; never expose on read models.\n\t\t\treturn \"never\";\n\t\tdefault:\n\t\t\treturn fallback;\n\t}\n}\n\n/**\n * Returns the runtime type kind for a field — used by {@link schemaFieldType}\n * to build structural types at runtime.\n */\nexport type FieldTypeKind =\n\t| \"string\"\n\t| \"number\"\n\t| \"boolean\"\n\t| \"string-array\"\n\t| \"json\"\n\t| \"relation\"\n\t| \"relation-many\"\n\t| \"password\"\n\t| \"unknown\";\n\n/** Map a server field to its runtime type kind. */\nexport function fieldTypeKind(field: SchemaField): FieldTypeKind {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\tcase \"select\":\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"multi_select\":\n\t\tcase \"multi_file\":\n\t\t\treturn \"string-array\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"json\";\n\t\tcase \"relation\":\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"relation-many\" : \"relation\";\n\t\tcase \"password\":\n\t\t\treturn \"password\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Derive a TypeScript field type from a {@link SchemaField} — the runtime\n * counterpart to the codegen mapper. Lets consumers build typed clients\n * from a fetched schema without running the CLI.\n */\nexport function schemaFieldType(field: SchemaField): unknown {\n\tswitch (fieldTypeKind(field)) {\n\t\tcase \"string\":\n\t\t\treturn String;\n\t\tcase \"number\":\n\t\t\treturn Number;\n\t\tcase \"boolean\":\n\t\t\treturn Boolean;\n\t\tcase \"string-array\":\n\t\t\treturn [String] as const;\n\t\tcase \"relation\":\n\t\t\treturn String;\n\t\tcase \"relation-many\":\n\t\t\treturn [String] as const;\n\t\tcase \"json\":\n\t\t\treturn Object;\n\t\tcase \"password\":\n\t\t\treturn undefined;\n\t\tcase \"unknown\":\n\t\t\treturn undefined;\n\t}\n}\n","// ── Codegen ─────────────────────────────────────────────\n// Generates a `lazypock.types.ts` module from the live API schema.\n//\n// The generated file exports:\n// - One interface per collection (e.g. `PostsRecord`)\n// - A `LazypockCollections` map: { posts: PostsRecord; users: UsersRecord; ... }\n// - A `createClient()` factory pre-bound to those types, so\n// `client.collection(\"posts\").create({ title: \"x\" })` is fully type-checked.\n//\n// The CLI in `src/cli.ts` wires this to `GET /collections`.\n\nimport type { CollectionSchema, SchemaField } from \"./schema\";\nimport { fieldTypeScriptType } from \"./typegen\";\n\n/** Format a raw collection name into a valid TS identifier (PascalCase). */\nexport function collectionTypeName(name: string): string {\n\treturn name\n\t\t.split(/[^a-zA-Z0-9]+/)\n\t\t.filter(Boolean)\n\t\t.map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n\t\t.join(\"\")\n\t\t.replace(/^[0-9]/, \"_$&\");\n}\n\n/** Interface member name — sanitize hyphens/spaces but keep readability. */\nexport function fieldKey(name: string): string {\n\treturn name.replace(/[^a-zA-Z0-9_]/g, \"_\");\n}\n\n\n\n/**\n * Generate the full TypeScript source for the typed SDK module.\n *\n * @param collections Collections fetched from the API.\n * @param options Generation options.\n */\nexport function generateTypes(\n\tcollections: CollectionSchema[],\n\toptions: {\n\t\t/** Import specifier for the lazypock package (default `lazypock`). */\n\t\tpackageName?: string;\n\t\t/** Emit base record fields (id, created, updated, …). Default true. */\n\t\tincludeBaseFields?: boolean;\n\t\t/** Skip system collections (names starting with `_` or `users`). Default false. */\n\t\tskipSystem?: boolean;\n\t} = {},\n): string {\n\tconst {\n\t\tpackageName = \"lazypock\",\n\t\tincludeBaseFields = true,\n\t\tskipSystem = false,\n\t} = options;\n\n\tconst filtered = skipSystem\n\t\t? collections.filter(\n\t\t\t\t(c) => !c.system && !c.name.startsWith(\"_\") && c.name !== \"users\",\n\t\t\t)\n\t\t: collections;\n\n\tconst sections: string[] = [];\n\tsections.push(`// ── Auto-generated by lazypock-ts ──────────────────────────\n// Do not edit by hand. Regenerate with: npx lazypock-gen\n// Schema snapshot: ${new Date().toISOString()}`);\n\n\tif (includeBaseFields) {\n\t\tsections.push(`export interface BaseRecord {\n id: string;\n collectionId: string;\n collectionName: string;\n created: string;\n updated: string;\n}`);\n\t}\n\n\t// One interface per collection\n\tfor (const coll of filtered) {\n\t\tconst typeName = collectionTypeName(coll.name);\n\t\tconst fields = coll.fields ?? [];\n\t\tconst lines = fields.map((f) => memberLine(f)).filter((l) => l !== \"\");\n\t\tconst body = lines.join(\"\\n\");\n\t\tsections.push(\n\t\t\t`export interface ${typeName}Record${renderInterface({\n\t\t\t\textends: includeBaseFields ? \"BaseRecord\" : undefined,\n\t\t\t\tbody,\n\t\t\t})}`,\n\t\t);\n\n\t\t// Write-only create data: what `create()`/`update()` accept. This\n\t\t// intentionally includes password fields (and hidden fields) that are\n\t\t// excluded from the read model — the server accepts them on write,\n\t\t// hashes passwords, and never returns them in responses.\n\t\tconst createLines: string[] = [];\n\t\tconst seenKeys = new Set<string>();\n\t\tfor (const f of fields) {\n\t\t\tfor (const { key, line } of createDataMemberLines(f)) {\n\t\t\t\tif (seenKeys.has(key)) continue;\n\t\t\t\tseenKeys.add(key);\n\t\t\t\tcreateLines.push(line);\n\t\t\t}\n\t\t}\n\t\tsections.push(\n\t\t\t`export interface ${typeName}CreateData${renderInterface({\n\t\t\t\tbody: createLines.join(\"\\n\"),\n\t\t\t})}`,\n\t\t);\n\t}\n\n\t// Auth collection type\n\tsections.push(`export interface AuthRecord extends BaseRecord {\n email: string;\n verified: boolean;\n}`);\n\n\t// Collections map\n\tconst mapEntries = filtered\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` \"${c.name}\": ${collectionTypeName(c.name)}Record${\n\t\t\t\t\tc.type === \"auth\" ? \" & AuthRecord\" : \"\"\n\t\t\t\t};`,\n\t\t)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCollections {\n${mapEntries}\n}`);\n\n\t// Create-data map — typed `create()`/`update()` payloads, including\n\t// write-only password fields.\n\tconst createMapEntries = filtered\n\t\t.map((c) => ` \"${c.name}\": ${collectionTypeName(c.name)}CreateData;`)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCreateData {\n${createMapEntries}\n}`);\n\n\t// Runtime schema snapshot — lets the client exclude hidden fields by\n\t// default, validate select/expand, and derive visible field lists.\n\tconst schemaEntries = collections\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` {\n id: ${JSON.stringify(c.id ?? null)},\n name: ${JSON.stringify(c.name)},\n type: ${JSON.stringify(c.type)},\n system: ${JSON.stringify(c.system ?? false)},\n fields: ${JSON.stringify(c.fields ?? [], null, 2)},\n rules: ${JSON.stringify(c.rules ?? {})},\n options: ${JSON.stringify(c.options ?? {})},\n }`,\n\t\t)\n\t\t.join(\",\\n\");\n\tsections.push(`// Schema snapshot (runtime) — hidden-field exclusion + query validation.\nexport const lazypockSchema: import(\"${packageName}\").CollectionSchema[] = [\n${schemaEntries}\n];`);\n\n\t// createClient factory\n\tsections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from \"${packageName}\";\n\n/**\n * Create a Lazypock client typed against this schema snapshot.\n * Collection access is fully type-checked:\n * client.collection(\"posts\").create({ title: \"x\" }) // title must exist\n *\n * For auth collections the generated *CreateData type includes the\n * write-only password field, so creating a user is type-safe:\n * client.collection(\"users\").create({ email, password }) // ✓\n *\n * The schema snapshot is wired into the client automatically, so hidden\n * fields are excluded from responses and select/expand are validated.\n */\nexport function createClient(options: LazypockClientOptions): TypedClient {\n return new TypedClient({\n ...options,\n types: {\n ...options.types,\n schemas: options.types?.schemas ?? lazypockSchema,\n },\n });\n}\n\nexport class TypedClient extends LazypockClient {\n // The constraint is \"keyof LazypockCollections | (string & {})\" rather than a\n // bare generic string or a strict keyof:\n // - the keyof LazypockCollections member is what makes the IDE suggest\n // collection names, and known literals resolve to the typed service:\n // client.collection(\"posts\") // suggested + typed\n // - the string & {} member keeps unknown/dynamic names compiling — they\n // resolve to the untyped service, so a studio that manages\n // user-created collections can still call collection(someString).\n //\n // The (string & {}) intersection keeps the union from collapsing to plain\n // string (which would silently kill the suggestions).\n //\n // create()/update() take the collection's *CreateData — the read model\n // omits password/hidden fields, but the write model carries them.\n override collection<K extends keyof LazypockCollections | (string & {})>(\n name: K,\n ): K extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[K], LazypockCreateData[K]>\n : CollectionService<unknown> {\n return super.collection(name) as K extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[K], LazypockCreateData[K]>\n : CollectionService<unknown>;\n }\n}\n`);\n\n\treturn sections.join(\"\\n\\n\") + \"\\n\";\n}\n\n/**\n * Render the interface body including the extends clause.\n * Empty body → ` extends BaseRecord {}` (valid TS).\n */\nfunction renderInterface(opts: { extends?: string; body: string }): string {\n\tconst ext = opts.extends ? ` extends ${opts.extends}` : \"\";\n\tif (!opts.body) return `${ext} {}`;\n\treturn `${ext} {\\n${opts.body}\\n}`;\n}\n\n/** Render a single interface member line for a field. */\nfunction memberLine(f: SchemaField): string {\n\t// Hidden fields are excluded from API responses by the schema-aware\n\t// client default — don't expose them on the read model either.\n\tif (f.hidden) return \"\";\n\tconst key = fieldKey(f.name);\n\tconst req = f.required || f.type === \"password\" ? \"\" : \"?\";\n\tconst type = fieldTypeScriptType(f);\n\t// `never` members (passwords) are omitted from read models.\n\tif (type === \"never\") return \"\";\n\treturn ` ${JSON.stringify(key)}${req}: ${type};`;\n}\n\n/**\n * Render the write-only create-data members for a field.\n *\n * Unlike the read model, create/update data includes password fields (sent as\n * plain strings — the server hashes them) and hidden fields: the server\n * accepts them on write and never returns them in responses.\n *\n * Password fields are exposed under the canonical PocketBase API key\n * `password` (the server accepts `password` for any single password field and\n * hashes it into the backing column, e.g. `password_hash`), with the raw\n * metadata name kept as a backward-compat alias.\n *\n * A field is only marked required when it actually needs a client value:\n * fields with a server-side default (`options.defaultValue`) and the auth\n * system fields `verified` / `emailVisibility` (the server defaults them to\n * false/true even though the snapshot doesn't carry `defaultValue`) stay\n * optional, so `create({ email, password })` typechecks without forcing\n * callers to send values the server would fill in anyway. Password fields\n * are optional (accounts may exist without a password, e.g. OAuth-only).\n */\nfunction createDataMemberLines(\n\tf: SchemaField,\n): { key: string; line: string }[] {\n\t// Autodate columns are server-managed; never writable.\n\tif (f.type === \"autodate\") return [];\n\tconst rawName = fieldKey(f.name);\n\tconst serverDefaulted =\n\t\tf.options?.defaultValue !== undefined ||\n\t\t(f.system && (f.name === \"verified\" || f.name === \"emailVisibility\"));\n\tconst req = f.required && !serverDefaulted ? \"\" : \"?\";\n\tconst type = f.type === \"password\" ? \"string\" : fieldTypeScriptType(f);\n\tif (type === \"never\") return [];\n\n\tconst members = [\n\t\t{ key: rawName, line: ` ${JSON.stringify(rawName)}${req}: ${type};` },\n\t];\n\t// Write-only password: canonical key is `password` (PocketBase parity) —\n\t// the server accepts `password` for any single password field and hashes\n\t// it. The raw metadata name (e.g. `password_hash`) stays as a compat alias.\n\tif (f.type === \"password\" && rawName !== \"password\") {\n\t\tmembers.unshift({\n\t\t\tkey: \"password\",\n\t\t\tline: ` ${JSON.stringify(\"password\")}${req}: ${type};`,\n\t\t});\n\t}\n\treturn members;\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -61,8 +61,10 @@ var HttpClient = class {
|
|
|
61
61
|
/**
|
|
62
62
|
* @param baseUrl The API base URL (e.g. `http://localhost:4000/api`). Trailing slash stripped.
|
|
63
63
|
* @param authStore The auth store providing the token for Authorization headers.
|
|
64
|
+
* @param connectionId Optional per-client connection id, sent as the
|
|
65
|
+
* `X-Connection-Id` header on every request (realtime origin-exclusion).
|
|
64
66
|
*/
|
|
65
|
-
constructor(baseUrl, authStore) {
|
|
67
|
+
constructor(baseUrl, authStore, connectionId) {
|
|
66
68
|
/**
|
|
67
69
|
* Abort controllers for in-flight requests, keyed by their cancellation key
|
|
68
70
|
* (default `METHOD path`). A new request with the same key aborts the
|
|
@@ -80,6 +82,7 @@ var HttpClient = class {
|
|
|
80
82
|
this.enableAutoCancellation = true;
|
|
81
83
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
82
84
|
this.authStore = authStore;
|
|
85
|
+
this.connectionId = connectionId;
|
|
83
86
|
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
84
87
|
}
|
|
85
88
|
async refreshAuth() {
|
|
@@ -232,6 +235,9 @@ var HttpClient = class {
|
|
|
232
235
|
if (this.authStore.token) {
|
|
233
236
|
headers["Authorization"] = "Bearer " + this.authStore.token;
|
|
234
237
|
}
|
|
238
|
+
if (this.connectionId && !headers["X-Connection-Id"] && !headers["x-connection-id"]) {
|
|
239
|
+
headers["X-Connection-Id"] = this.connectionId;
|
|
240
|
+
}
|
|
235
241
|
const init = {
|
|
236
242
|
method,
|
|
237
243
|
headers,
|
|
@@ -817,25 +823,24 @@ var CollectionService = class _CollectionService {
|
|
|
817
823
|
derived.fieldsPreset = this.fieldsPreset;
|
|
818
824
|
return derived;
|
|
819
825
|
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Subscribe to realtime changes for this collection.
|
|
823
|
-
* The event's `action` is one of `"create" | "update" | "delete"`.
|
|
824
|
-
*
|
|
825
|
-
* Access is governed by the collection's `listRule` (PocketBase semantics):
|
|
826
|
-
* public collections allow anonymous subscriptions; other collections
|
|
827
|
-
* require a matching logged-in user or superuser.
|
|
828
|
-
*
|
|
829
|
-
* @param callback Received on every record change.
|
|
830
|
-
* @param recordId Optional — subscribe to a single record instead of `*`.
|
|
831
|
-
* @returns A function that unsubscribes this callback.
|
|
832
|
-
*/
|
|
833
|
-
subscribe(callback, recordId) {
|
|
826
|
+
subscribe(topicOrCallback, maybeCallback, options) {
|
|
834
827
|
if (!this.realtime) {
|
|
835
828
|
console.warn("[lazypock] No realtime service configured.");
|
|
836
829
|
return () => {
|
|
837
830
|
};
|
|
838
831
|
}
|
|
832
|
+
let recordId;
|
|
833
|
+
let callback;
|
|
834
|
+
let joinPayload;
|
|
835
|
+
if (typeof topicOrCallback === "function") {
|
|
836
|
+
callback = topicOrCallback;
|
|
837
|
+
if (typeof maybeCallback === "string") recordId = maybeCallback;
|
|
838
|
+
} else {
|
|
839
|
+
callback = maybeCallback;
|
|
840
|
+
recordId = topicOrCallback === "*" ? void 0 : topicOrCallback;
|
|
841
|
+
const { headers: _ignored, ...rest } = options ?? {};
|
|
842
|
+
if (Object.keys(rest).length > 0) joinPayload = rest;
|
|
843
|
+
}
|
|
839
844
|
const topic = "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
|
|
840
845
|
const handler = (raw) => {
|
|
841
846
|
const record = raw.payload?.["record"] ?? {};
|
|
@@ -846,15 +851,23 @@ var CollectionService = class _CollectionService {
|
|
|
846
851
|
});
|
|
847
852
|
};
|
|
848
853
|
this.realtime.ensureConnected();
|
|
849
|
-
this.realtime.subscribe(topic, handler);
|
|
854
|
+
this.realtime.subscribe(topic, handler, joinPayload);
|
|
850
855
|
return () => this.realtime?.unsubscribe(topic, handler);
|
|
851
856
|
}
|
|
852
857
|
/**
|
|
853
|
-
* Unsubscribe
|
|
854
|
-
*
|
|
858
|
+
* Unsubscribe from realtime changes (PocketBase-compatible):
|
|
859
|
+
* - `unsubscribe()` — remove **all** subscriptions of this collection
|
|
860
|
+
* - `unsubscribe('*')` — remove wildcard subscriptions
|
|
861
|
+
* - `unsubscribe('RECORD_ID')` — remove that record's subscriptions
|
|
855
862
|
*/
|
|
856
863
|
unsubscribe(recordId) {
|
|
857
|
-
|
|
864
|
+
if (recordId === void 0) {
|
|
865
|
+
this.realtime?.unsubscribeByPrefix(
|
|
866
|
+
"collection:" + this.collectionName
|
|
867
|
+
);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const topic = "collection:" + this.collectionName + (recordId === "*" ? "" : ":" + recordId);
|
|
858
871
|
this.realtime?.unsubscribe(topic);
|
|
859
872
|
}
|
|
860
873
|
// ── Auth Collection Methods ──
|
|
@@ -921,6 +934,7 @@ var RealtimeService = class {
|
|
|
921
934
|
this.reconnectAttempt = 0;
|
|
922
935
|
this.maxReconnectDelay = 5e3;
|
|
923
936
|
this.url = "";
|
|
937
|
+
this.tokenProvider = null;
|
|
924
938
|
// ── Heartbeat ──
|
|
925
939
|
this.heartbeatInterval = null;
|
|
926
940
|
}
|
|
@@ -939,6 +953,16 @@ var RealtimeService = class {
|
|
|
939
953
|
get lastToken() {
|
|
940
954
|
return this.token;
|
|
941
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Set the per-client connection id (browser tab / device). The SDK
|
|
958
|
+
* appends it as the `connectionId` socket query param at connect time
|
|
959
|
+
* (unless the URL already carries one), matching the `X-Connection-Id`
|
|
960
|
+
* header the HTTP client sends — together they let the server exclude
|
|
961
|
+
* the originating connection from its own realtime broadcasts.
|
|
962
|
+
*/
|
|
963
|
+
setConnectionId(id) {
|
|
964
|
+
this.connectionId = id;
|
|
965
|
+
}
|
|
942
966
|
/**
|
|
943
967
|
* Set the socket URL. Useful before subscribing so the SDK can
|
|
944
968
|
* auto-connect on the first {@link subscribe}.
|
|
@@ -946,6 +970,32 @@ var RealtimeService = class {
|
|
|
946
970
|
setUrl(url) {
|
|
947
971
|
this.url = url;
|
|
948
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* Register a token provider consulted at every (re)connect.
|
|
975
|
+
* When set, it takes precedence over the token passed to {@link connect}.
|
|
976
|
+
*/
|
|
977
|
+
setTokenProvider(provider) {
|
|
978
|
+
this.tokenProvider = provider;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Reconnect the socket immediately with the current token.
|
|
982
|
+
* Called by the SDK when auth changes (login/logout/token refresh) so
|
|
983
|
+
* private-channel joins are authorized with the new credentials. No-op
|
|
984
|
+
* when the socket has never been opened and nothing is subscribed.
|
|
985
|
+
*/
|
|
986
|
+
refresh() {
|
|
987
|
+
if (typeof WebSocket === "undefined") return;
|
|
988
|
+
if (!this.ws && this.subscriptions.size === 0) return;
|
|
989
|
+
this.clearReconnectTimer();
|
|
990
|
+
const ws = this.ws;
|
|
991
|
+
this.ws = null;
|
|
992
|
+
if (ws) {
|
|
993
|
+
ws.onclose = null;
|
|
994
|
+
ws.close();
|
|
995
|
+
}
|
|
996
|
+
this.reconnectAttempt = 0;
|
|
997
|
+
this.doConnect();
|
|
998
|
+
}
|
|
949
999
|
/*
|
|
950
1000
|
* Ensure the socket is connected, then subscribe.
|
|
951
1001
|
* Used by collection-level convenience wrappers so a connection is opened
|
|
@@ -969,15 +1019,30 @@ var RealtimeService = class {
|
|
|
969
1019
|
this.ws = null;
|
|
970
1020
|
}
|
|
971
1021
|
/**
|
|
972
|
-
* Subscribe to a topic (e.g. "collection:posts" or "
|
|
1022
|
+
* Subscribe to a topic (e.g. "collection:posts" or "custom:chat-room").
|
|
973
1023
|
* The backend Channel authorizes via listRule on join.
|
|
1024
|
+
*
|
|
1025
|
+
* @param joinPayload Optional payload forwarded with the channel join
|
|
1026
|
+
* (available to the server's join callback / hooks, e.g. `expand`).
|
|
974
1027
|
*/
|
|
975
|
-
subscribe(topic, callback) {
|
|
1028
|
+
subscribe(topic, callback, joinPayload) {
|
|
976
1029
|
const subs = this.subscriptions.get(topic) || [];
|
|
977
|
-
subs.push({ topic, callback });
|
|
1030
|
+
subs.push({ topic, callback, joinPayload });
|
|
978
1031
|
this.subscriptions.set(topic, subs);
|
|
979
1032
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
980
|
-
this.joinTopic(topic);
|
|
1033
|
+
this.joinTopic(topic, joinPayload);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Remove all subscriptions for topics under `prefix` (the topic itself or
|
|
1038
|
+
* any topic starting with `prefix + ":"`). Used by collection-level
|
|
1039
|
+
* `unsubscribe()` to drop every subscription of a collection.
|
|
1040
|
+
*/
|
|
1041
|
+
unsubscribeByPrefix(prefix) {
|
|
1042
|
+
for (const topic of [...this.subscriptions.keys()]) {
|
|
1043
|
+
if (topic === prefix || topic.startsWith(prefix + ":")) {
|
|
1044
|
+
this.subscriptions.delete(topic);
|
|
1045
|
+
}
|
|
981
1046
|
}
|
|
982
1047
|
}
|
|
983
1048
|
/**
|
|
@@ -996,9 +1061,14 @@ var RealtimeService = class {
|
|
|
996
1061
|
}
|
|
997
1062
|
}
|
|
998
1063
|
resubscribeAll() {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1064
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1065
|
+
for (const entries of this.subscriptions.values()) {
|
|
1066
|
+
for (const entry of entries) {
|
|
1067
|
+
if (seen.has(entry.topic)) continue;
|
|
1068
|
+
seen.add(entry.topic);
|
|
1069
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1070
|
+
this.joinTopic(entry.topic, entry.joinPayload);
|
|
1071
|
+
}
|
|
1002
1072
|
}
|
|
1003
1073
|
}
|
|
1004
1074
|
}
|
|
@@ -1010,8 +1080,12 @@ var RealtimeService = class {
|
|
|
1010
1080
|
return;
|
|
1011
1081
|
}
|
|
1012
1082
|
let url = this.url;
|
|
1013
|
-
|
|
1014
|
-
|
|
1083
|
+
const token = this.tokenProvider ? this.tokenProvider() : this.token;
|
|
1084
|
+
if (token) {
|
|
1085
|
+
url += (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(token);
|
|
1086
|
+
}
|
|
1087
|
+
if (this.connectionId && !url.includes("connectionId=")) {
|
|
1088
|
+
url += (url.includes("?") ? "&" : "?") + "connectionId=" + encodeURIComponent(this.connectionId);
|
|
1015
1089
|
}
|
|
1016
1090
|
this.ws = new WebSocket(url);
|
|
1017
1091
|
this.ws.onopen = () => {
|
|
@@ -1058,12 +1132,12 @@ var RealtimeService = class {
|
|
|
1058
1132
|
}
|
|
1059
1133
|
}
|
|
1060
1134
|
}
|
|
1061
|
-
joinTopic(topic) {
|
|
1135
|
+
joinTopic(topic, joinPayload) {
|
|
1062
1136
|
const ref = this.nextRef();
|
|
1063
1137
|
const msg = JSON.stringify({
|
|
1064
1138
|
topic,
|
|
1065
1139
|
event: "phx_join",
|
|
1066
|
-
payload: {},
|
|
1140
|
+
payload: joinPayload ?? {},
|
|
1067
1141
|
ref
|
|
1068
1142
|
});
|
|
1069
1143
|
this.ws?.send(msg);
|
|
@@ -1559,26 +1633,27 @@ export function createClient(options: LazypockClientOptions): TypedClient {
|
|
|
1559
1633
|
}
|
|
1560
1634
|
|
|
1561
1635
|
export class TypedClient extends LazypockClient {
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1636
|
+
// The constraint is "keyof LazypockCollections | (string & {})" rather than a
|
|
1637
|
+
// bare generic string or a strict keyof:
|
|
1638
|
+
// - the keyof LazypockCollections member is what makes the IDE suggest
|
|
1639
|
+
// collection names, and known literals resolve to the typed service:
|
|
1640
|
+
// client.collection("posts") // suggested + typed
|
|
1641
|
+
// - the string & {} member keeps unknown/dynamic names compiling \u2014 they
|
|
1642
|
+
// resolve to the untyped service, so a studio that manages
|
|
1643
|
+
// user-created collections can still call collection(someString).
|
|
1567
1644
|
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
// T extends string (rather than keyof LazypockCollections) with a conditional
|
|
1571
|
-
// return type, so the IDE suggests collection names AND unknown/dynamic names
|
|
1572
|
-
// still resolve to the untyped service \u2014 a studio that manages
|
|
1573
|
-
// user-created collections must be able to call collection(someString).
|
|
1645
|
+
// The (string & {}) intersection keeps the union from collapsing to plain
|
|
1646
|
+
// string (which would silently kill the suggestions).
|
|
1574
1647
|
//
|
|
1575
1648
|
// create()/update() take the collection's *CreateData \u2014 the read model
|
|
1576
1649
|
// omits password/hidden fields, but the write model carries them.
|
|
1577
|
-
override collection<
|
|
1578
|
-
|
|
1650
|
+
override collection<K extends keyof LazypockCollections | (string & {})>(
|
|
1651
|
+
name: K,
|
|
1652
|
+
): K extends keyof LazypockCollections
|
|
1653
|
+
? CollectionService<LazypockCollections[K], LazypockCreateData[K]>
|
|
1579
1654
|
: CollectionService<unknown> {
|
|
1580
|
-
return super.collection(name) as
|
|
1581
|
-
? CollectionService<LazypockCollections[
|
|
1655
|
+
return super.collection(name) as K extends keyof LazypockCollections
|
|
1656
|
+
? CollectionService<LazypockCollections[K], LazypockCreateData[K]>
|
|
1582
1657
|
: CollectionService<unknown>;
|
|
1583
1658
|
}
|
|
1584
1659
|
}
|
|
@@ -1620,6 +1695,15 @@ function createDataMemberLines(f) {
|
|
|
1620
1695
|
}
|
|
1621
1696
|
|
|
1622
1697
|
// src/lazypock.ts
|
|
1698
|
+
function createConnectionId() {
|
|
1699
|
+
try {
|
|
1700
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1701
|
+
return crypto.randomUUID();
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
return "conn-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1706
|
+
}
|
|
1623
1707
|
var LazypockClient = class {
|
|
1624
1708
|
/**
|
|
1625
1709
|
* Create a new Lazypock client.
|
|
@@ -1630,11 +1714,16 @@ var LazypockClient = class {
|
|
|
1630
1714
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1631
1715
|
this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
|
|
1632
1716
|
this.authReady = options.authStore ? Promise.resolve() : this.authStore.init();
|
|
1633
|
-
|
|
1717
|
+
const connectionId = createConnectionId();
|
|
1718
|
+
this.http = new HttpClient(baseUrl, this.authStore, connectionId);
|
|
1634
1719
|
this.realtime = options.realtime ?? new RealtimeService();
|
|
1635
1720
|
if (!options.realtime) {
|
|
1636
1721
|
this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
|
|
1637
1722
|
}
|
|
1723
|
+
this.realtime.setConnectionId(connectionId);
|
|
1724
|
+
this.realtime.setTokenProvider(() => this.authStore.token);
|
|
1725
|
+
this.authStore.onChange(() => this.realtime.refresh());
|
|
1726
|
+
this.authReady.then(() => this.realtime.refresh());
|
|
1638
1727
|
this.files = new FilesService(this.http);
|
|
1639
1728
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
1640
1729
|
if (options.types?.schemas) {
|