@burojs/pocketbase 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Kuznetsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @burojs/pocketbase
2
+
3
+ A [PocketBase](https://pocketbase.io/) data provider and auth provider for
4
+ [buro](https://github.com/buro/buro).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add @burojs/pocketbase @burojs/core
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { createPocketbaseAuthProvider, createPocketbaseDataProvider } from '@burojs/pocketbase';
16
+
17
+ const authProvider = createPocketbaseAuthProvider({ baseUrl: pbUrl });
18
+
19
+ const dataProvider = createPocketbaseDataProvider({
20
+ baseUrl: pbUrl,
21
+ getToken: authProvider.getToken,
22
+ });
23
+ ```
24
+
25
+ ### `createPocketbaseDataProvider({ baseUrl, getToken, fetchImpl? })`
26
+
27
+ A `DataProvider` over PocketBase's REST API: `list`/`one`/`many` queries and
28
+ `createOne`/`updateOne`/`deleteOne` mutations. Portable filter and sort
29
+ descriptors are translated to PocketBase's own filter string syntax; offset
30
+ pagination only — cursor pagination is refused with a `capability` error.
31
+ `filterExpression` (the filter-string translator) is also exported on its own,
32
+ for callers that need to build the same string without the rest of the
33
+ provider. It returns `undefined` for a filter that **matches every record** —
34
+ send no `filter` argument for that — and a real expression string for
35
+ everything else, including a filter that matches nothing. A branchless
36
+ combinator (`{and: []}`, `{or: []}`) and an empty-set test (`in: []`,
37
+ `hasAny: []`, `notIn: []`, `hasAll: []`) carry truth values and are folded, not
38
+ dropped; PocketBase's grammar has no boolean literal, so the one constant that
39
+ survives folding — FALSE — is emitted as the contradiction `(id="" && id!="")`.
40
+
41
+ ### `createPocketbaseAuthProvider({ baseUrl, fetchImpl?, storageKey? })`
42
+
43
+ Password auth against a PocketBase `users` collection, with the session kept
44
+ in `localStorage`. Returns an `AuthProvider` plus a `getToken(): string | null`
45
+ escape hatch, which is how the data provider above learns the current token.
46
+ Deliberately small: this exists to demonstrate the data/auth provider ports,
47
+ not to be a complete auth reference.
48
+
49
+ ## License
50
+
51
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ var core = require('@burojs/core');
4
+
5
+ // src/data-provider.ts
6
+ function lit(value) {
7
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
8
+ }
9
+ var FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
10
+ function unsupportedOperator(field, op) {
11
+ return {
12
+ kind: "capability",
13
+ code: FILTER_OPERATOR_UNSUPPORTED_CODE,
14
+ message: `@burojs/pocketbase does not support the filter operator '${op}' (on '${field}').`
15
+ };
16
+ }
17
+ var DROP = { kind: "drop" };
18
+ var constant = (value) => ({ kind: "const", value });
19
+ var MATCHES_NO_RECORD = '(id="" && id!="")';
20
+ function combine(branches, op) {
21
+ if (branches.length === 0) return constant(op === "and");
22
+ const absorbing = op === "or";
23
+ const nodes = branches.map(translateFilter);
24
+ if (nodes.some((node) => node.kind === "const" && node.value === absorbing)) {
25
+ return constant(absorbing);
26
+ }
27
+ const parts = nodes.flatMap((node) => node.kind === "expr" ? [node.text] : []);
28
+ if (parts.length > 0)
29
+ return { kind: "expr", text: `(${parts.join(op === "and" ? " && " : " || ")})` };
30
+ return nodes.some((node) => node.kind === "const") ? constant(!absorbing) : DROP;
31
+ }
32
+ function translateFilter(filter) {
33
+ if (filter === null || typeof filter !== "object") return DROP;
34
+ const candidate = filter;
35
+ if (Array.isArray(candidate.and)) return combine(candidate.and, "and");
36
+ if (Array.isArray(candidate.or)) return combine(candidate.or, "or");
37
+ if ("not" in candidate) {
38
+ const inner = translateFilter(candidate.not);
39
+ if (inner.kind === "drop") return DROP;
40
+ if (inner.kind === "const") return constant(!inner.value);
41
+ return { kind: "expr", text: `!(${inner.text})` };
42
+ }
43
+ if (typeof candidate.field !== "string" || typeof candidate.op !== "string") return DROP;
44
+ const { field, op, value } = candidate;
45
+ if (op === "isNull") return { kind: "expr", text: `${field}=null` };
46
+ if (op === "isNotNull") return { kind: "expr", text: `${field}!=null` };
47
+ if (!("value" in candidate) || value === void 0) return DROP;
48
+ const chain = (join, term, empty) => {
49
+ if (!Array.isArray(value)) return DROP;
50
+ if (value.length === 0) return constant(empty);
51
+ return { kind: "expr", text: `(${value.map(term).join(join)})` };
52
+ };
53
+ switch (op) {
54
+ case "eq":
55
+ return { kind: "expr", text: `${field}=${lit(value)}` };
56
+ case "neq":
57
+ return { kind: "expr", text: `${field}!=${lit(value)}` };
58
+ case "contains":
59
+ return { kind: "expr", text: `${field}~${lit(value)}` };
60
+ case "startsWith":
61
+ return { kind: "expr", text: `${field}~${lit(`${String(value)}%`)}` };
62
+ case "endsWith":
63
+ return { kind: "expr", text: `${field}~${lit(`%${String(value)}`)}` };
64
+ case "gt":
65
+ return { kind: "expr", text: `${field}>${lit(value)}` };
66
+ case "gte":
67
+ return { kind: "expr", text: `${field}>=${lit(value)}` };
68
+ case "lt":
69
+ return { kind: "expr", text: `${field}<${lit(value)}` };
70
+ case "lte":
71
+ return { kind: "expr", text: `${field}<=${lit(value)}` };
72
+ case "in":
73
+ return chain(" || ", (entry) => `${field}=${lit(entry)}`, false);
74
+ case "notIn":
75
+ return chain(" && ", (entry) => `${field}!=${lit(entry)}`, true);
76
+ case "has":
77
+ return { kind: "expr", text: `${field}~${lit(value)}` };
78
+ case "hasAny":
79
+ return chain(" || ", (entry) => `${field}~${lit(entry)}`, false);
80
+ case "hasAll":
81
+ return chain(" && ", (entry) => `${field}~${lit(entry)}`, true);
82
+ // An operator this provider cannot express fails LOUDLY, and with the same
83
+ // `kind` and `code` `@burojs/hasura` uses. It used to return DROP, i.e. "no
84
+ // constraint" — so an unknown operator quietly matched every record here
85
+ // while the sibling quietly turned it into `_eq`. Two silent, DIFFERENT
86
+ // wrong answers for one input is the same defect class as the branchless
87
+ // combinators, one hop out.
88
+ default:
89
+ throw unsupportedOperator(field, op);
90
+ }
91
+ }
92
+ function filterExpression(filter) {
93
+ const node = translateFilter(filter);
94
+ if (node.kind === "expr") return node.text;
95
+ if (node.kind === "const") return node.value ? void 0 : MATCHES_NO_RECORD;
96
+ return void 0;
97
+ }
98
+ function recordResult(record, idField) {
99
+ if (record === null || typeof record !== "object") {
100
+ throw {
101
+ kind: "protocol",
102
+ code: "invalid-record",
103
+ message: "PocketBase returned a non-object record."
104
+ };
105
+ }
106
+ const id = record[idField];
107
+ if (typeof id !== "string" && (typeof id !== "number" || !Number.isFinite(id))) {
108
+ throw {
109
+ kind: "protocol",
110
+ code: "invalid-record-id",
111
+ message: `PocketBase record is missing a valid ${idField} field.`
112
+ };
113
+ }
114
+ return { kind: "record", id, record, completeness: "full" };
115
+ }
116
+ function createPocketbaseDataProvider(opts) {
117
+ const base = opts.baseUrl.replace(/\/+$/, "");
118
+ const records = (resource) => `${base}/api/collections/${resource}/records`;
119
+ async function call(url, init = {}, signal) {
120
+ try {
121
+ const token = opts.getToken();
122
+ const headers = new Headers(init.headers);
123
+ if (token) headers.set("authorization", token);
124
+ if (init.body && !(init.body instanceof FormData))
125
+ headers.set("content-type", "application/json");
126
+ const response = await (opts.fetchImpl ?? fetch)(url, { ...init, headers, signal });
127
+ const text = await response.text();
128
+ let json = {};
129
+ if (text) {
130
+ try {
131
+ json = JSON.parse(text);
132
+ } catch {
133
+ throw {
134
+ kind: "protocol",
135
+ code: "invalid-json",
136
+ message: "PocketBase returned invalid JSON.",
137
+ status: response.status
138
+ };
139
+ }
140
+ }
141
+ if (!response.ok) {
142
+ throw {
143
+ kind: "transport",
144
+ code: "pocketbase-request-failed",
145
+ message: typeof json.message === "string" ? json.message : `PocketBase error ${response.status}`,
146
+ status: response.status,
147
+ details: json
148
+ };
149
+ }
150
+ return json;
151
+ } catch (error) {
152
+ if (error !== null && typeof error === "object" && "kind" in error) throw error;
153
+ throw {
154
+ kind: signal?.aborted ? "cancelled" : "transport",
155
+ code: signal?.aborted ? "request-aborted" : "pocketbase-request-failed",
156
+ message: error instanceof Error ? error.message : "PocketBase request failed.",
157
+ cause: error
158
+ };
159
+ }
160
+ }
161
+ const body = (input) => input instanceof FormData ? input : JSON.stringify(input);
162
+ return core.defineDataProvider({
163
+ queries: {
164
+ async list(descriptor, context) {
165
+ const params = new URLSearchParams();
166
+ if (descriptor.pagination?.kind === "offset") {
167
+ params.set("page", String(descriptor.pagination.page));
168
+ params.set("perPage", String(descriptor.pagination.pageSize));
169
+ } else if (descriptor.pagination?.kind === "cursor") {
170
+ throw {
171
+ kind: "capability",
172
+ code: "cursor-pagination-unsupported",
173
+ message: "PocketBase provider supports offset pagination only."
174
+ };
175
+ }
176
+ const filter = filterExpression(descriptor.filter);
177
+ if (filter) params.set("filter", filter);
178
+ if (descriptor.sort?.length) {
179
+ params.set(
180
+ "sort",
181
+ descriptor.sort.map(({ field, direction }) => `${direction === "desc" ? "-" : "+"}${field}`).join(",")
182
+ );
183
+ }
184
+ const json = await call(
185
+ `${records(descriptor.resource)}?${params}`,
186
+ {},
187
+ context.signal
188
+ );
189
+ return {
190
+ data: json.items ?? [],
191
+ ...typeof json.totalItems === "number" ? { total: json.totalItems } : {}
192
+ };
193
+ },
194
+ async one(descriptor, context) {
195
+ return {
196
+ data: await call(
197
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
198
+ {},
199
+ context.signal
200
+ )
201
+ };
202
+ },
203
+ async many(descriptor, context) {
204
+ if (descriptor.ids.length === 0) return { data: [] };
205
+ const filter = descriptor.ids.map((id) => `id=${lit(id)}`).join(" || ");
206
+ const json = await call(
207
+ `${records(descriptor.resource)}?filter=${encodeURIComponent(filter)}&perPage=${descriptor.ids.length}`,
208
+ {},
209
+ context.signal
210
+ );
211
+ return { data: json.items ?? [] };
212
+ }
213
+ },
214
+ mutations: {
215
+ async createOne(descriptor, context) {
216
+ const record = await call(
217
+ records(descriptor.resource),
218
+ { method: "POST", body: body(descriptor.input) },
219
+ context.signal
220
+ );
221
+ return recordResult(record, descriptor.idField);
222
+ },
223
+ async updateOne(descriptor, context) {
224
+ const record = await call(
225
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
226
+ { method: "PATCH", body: body(descriptor.input) },
227
+ context.signal
228
+ );
229
+ return recordResult(record, descriptor.idField);
230
+ },
231
+ async deleteOne(descriptor, context) {
232
+ await call(
233
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
234
+ { method: "DELETE" },
235
+ context.signal
236
+ );
237
+ return { kind: "identity", id: descriptor.id };
238
+ }
239
+ }
240
+ });
241
+ }
242
+
243
+ // src/auth-provider.ts
244
+ function createPocketbaseAuthProvider(opts) {
245
+ const base = opts.baseUrl.replace(/\/+$/, "");
246
+ const key = opts.storageKey ?? "buro-example-vite-auth";
247
+ const read = () => {
248
+ try {
249
+ const raw = localStorage.getItem(key);
250
+ if (!raw) return null;
251
+ const parsed = JSON.parse(raw);
252
+ if (!parsed || typeof parsed.token !== "string" || typeof parsed.record?.id !== "string") {
253
+ return null;
254
+ }
255
+ return parsed;
256
+ } catch {
257
+ return null;
258
+ }
259
+ };
260
+ return {
261
+ getToken: () => read()?.token ?? null,
262
+ async login(params) {
263
+ const { email, password } = params;
264
+ const fetchImpl = opts.fetchImpl ?? fetch;
265
+ const res = await fetchImpl(`${base}/api/collections/users/auth-with-password`, {
266
+ method: "POST",
267
+ headers: { "content-type": "application/json" },
268
+ body: JSON.stringify({ identity: email, password })
269
+ });
270
+ if (!res.ok) {
271
+ return {
272
+ success: false,
273
+ error: { kind: "auth", code: "invalid-credentials", message: "Invalid credentials" }
274
+ };
275
+ }
276
+ localStorage.setItem(key, JSON.stringify(await res.json()));
277
+ return { success: true };
278
+ },
279
+ async logout() {
280
+ localStorage.removeItem(key);
281
+ return { success: true };
282
+ },
283
+ async check() {
284
+ return read() ? { authenticated: true } : { authenticated: false, reason: "signed-out" };
285
+ },
286
+ async onError(error) {
287
+ return { action: "propagate", error };
288
+ },
289
+ async getIdentity() {
290
+ const session = read();
291
+ if (!session) return null;
292
+ return { id: session.record.id, name: session.record.name ?? session.record.email };
293
+ }
294
+ };
295
+ }
296
+
297
+ exports.FILTER_OPERATOR_UNSUPPORTED_CODE = FILTER_OPERATOR_UNSUPPORTED_CODE;
298
+ exports.createPocketbaseAuthProvider = createPocketbaseAuthProvider;
299
+ exports.createPocketbaseDataProvider = createPocketbaseDataProvider;
300
+ exports.filterExpression = filterExpression;
301
+ //# sourceMappingURL=index.cjs.map
302
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-provider.ts","../src/auth-provider.ts"],"names":["defineDataProvider"],"mappings":"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","file":"index.cjs","sourcesContent":["import { defineDataProvider, type DataError, type DataProvider, type RecordId } from '@burojs/core';\n\nexport interface PocketbaseDataProviderOpts {\n baseUrl: string;\n getToken: () => string | null;\n fetchImpl?: typeof fetch;\n}\n\n/** Escapes a value for a PocketBase filter string literal. */\nfunction lit(value: unknown): string {\n return `\"${String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * The shared refusal code for \"this provider cannot express that filter\n * operator\". `@burojs/hasura` exports the identical string as\n * `FILTER_OPERATOR_UNSUPPORTED_CODE` and throws the same `kind`, so an\n * application that handles one handles the other.\n *\n * The two packages do not import from each other (neither depends on the\n * other, and a shared constant would have to live in `@burojs/core`, which\n * owns the filter AST but not its transports). The agreement is therefore\n * pinned by a test that drives BOTH providers:\n * `examples/marketplace-ops/tests/provider-agreement.test.ts`.\n */\nexport const FILTER_OPERATOR_UNSUPPORTED_CODE = 'filter-operator-unsupported';\n\nfunction unsupportedOperator(field: string, op: string): DataError {\n return {\n kind: 'capability',\n code: FILTER_OPERATOR_UNSUPPORTED_CODE,\n message: `@burojs/pocketbase does not support the filter operator '${op}' (on '${field}').`,\n };\n}\n\n/**\n * What one filter node translated to. Three outcomes, not two — that missing\n * third outcome IS the defect this type exists to make unrepresentable.\n *\n * THE CLASS, stated once. The old signature was `string | undefined`, and\n * `undefined` carried two incompatible meanings at once:\n *\n * - \"no constraint was expressed here — drop me\" (no value supplied, an\n * unrecognised node), and\n * - \"this node is a constant\", which is what a BRANCHLESS COMBINATOR is:\n * `{and: []}` is vacuously TRUE, `{or: []}` is vacuously FALSE, `in: []`\n * is an empty-set membership test and therefore FALSE.\n *\n * Dropping a node is sound only when the node is TRUE and sits in an AND\n * position, where a TRUE conjunct is genuinely absorbed. Everywhere else —\n * under `not`, beside a disjunct in an `or`, or alone at the top level, where\n * the caller turns `undefined` into no `filter` param at all and the server\n * answers with EVERY row — dropping a false node inverts the query.\n *\n * The sibling `@burojs/hasura` closed the same class in `filtersToWhere` by\n * emitting `{_and: []}` / `{_or: []}` explicitly, because Hasura's `bool_exp`\n * has a literal for both truth values. PocketBase's filter grammar has none\n * (every term is `field op value`), so this half CANNOT emit a truth value as\n * a term; it folds instead and materialises only the one constant it cannot\n * fold away — see {@link MATCHES_NO_RECORD}.\n */\ntype FilterNode =\n | { readonly kind: 'expr'; readonly text: string }\n | { readonly kind: 'const'; readonly value: boolean }\n | { readonly kind: 'drop' };\n\nconst DROP: FilterNode = { kind: 'drop' };\nconst constant = (value: boolean): FilterNode => ({ kind: 'const', value });\n\n/**\n * A PocketBase filter expression that matches NO record.\n *\n * It has to be a real expression: PocketBase's filter grammar has no boolean\n * literal and no nullary operator — every term is `field op value`, combined\n * with `&&`, `||`, `!(…)` and parentheses (the grammar `@burojs/mock-kit`'s\n * `parsePbFilter` implements, and the one this file's own emitters produce).\n * So \"match nothing\" is spelled as a CONTRADICTION over a column that every\n * record has.\n *\n * Why this particular one: `id=\"\" && id!=\"\"` is false for every possible value\n * of `id`, by the definition of `=` / `!=` alone. It does not depend on what\n * `id` contains, on the collection's schema, or on PocketBase's own guarantee\n * that a record id is a non-empty 15-character string — that guarantee would\n * make the shorter `id=\"\"` sufficient, but the contradiction needs no such\n * premise and stays false under SQL three-valued logic too (a NULL comparison\n * is not TRUE, so the row is still excluded).\n *\n * There is no TRUE counterpart because none is ever needed: a TRUE node is\n * absorbed by every AND, collapses every OR to TRUE, and at the top level is\n * exactly \"send no `filter` param\", which already means every row.\n */\nconst MATCHES_NO_RECORD = '(id=\"\" && id!=\"\")';\n\n/**\n * Fold a combinator's branches. `op` supplies its own absorbing element\n * (FALSE for `and`, TRUE for `or`) and identity element (the other one).\n *\n * The branchless case is checked on the SOURCE array, before mapping, and is\n * deliberately NOT the same case as \"every branch mapped away because no VALUE\n * was supplied\". That second case still yields `drop`, and `@burojs/hasura`\n * drops on exactly the same condition — a missing or `undefined` value, never\n * on `''`, which both providers translate.\n */\nfunction combine(branches: readonly unknown[], op: 'and' | 'or'): FilterNode {\n if (branches.length === 0) return constant(op === 'and');\n const absorbing = op === 'or';\n const nodes = branches.map(translateFilter);\n if (nodes.some((node) => node.kind === 'const' && node.value === absorbing)) {\n return constant(absorbing);\n }\n const parts = nodes.flatMap((node) => (node.kind === 'expr' ? [node.text] : []));\n if (parts.length > 0)\n return { kind: 'expr', text: `(${parts.join(op === 'and' ? ' && ' : ' || ')})` };\n // Nothing left to write down. A surviving identity-element constant still\n // has to be reported as a constant (it may sit under a `not`); if there were\n // no constants at all, every branch was a drop and so is the combinator.\n return nodes.some((node) => node.kind === 'const') ? constant(!absorbing) : DROP;\n}\n\nfunction translateFilter(filter: unknown): FilterNode {\n if (filter === null || typeof filter !== 'object') return DROP;\n const candidate = filter as Record<string, unknown>;\n if (Array.isArray(candidate.and)) return combine(candidate.and, 'and');\n if (Array.isArray(candidate.or)) return combine(candidate.or, 'or');\n if ('not' in candidate) {\n const inner = translateFilter(candidate.not);\n if (inner.kind === 'drop') return DROP;\n if (inner.kind === 'const') return constant(!inner.value);\n return { kind: 'expr', text: `!(${inner.text})` };\n }\n if (typeof candidate.field !== 'string' || typeof candidate.op !== 'string') return DROP;\n\n const { field, op, value } = candidate;\n // `isNull`/`isNotNull` carry no value and are answered before the value\n // check, exactly as `@burojs/hasura` orders it.\n if (op === 'isNull') return { kind: 'expr', text: `${field}=null` };\n if (op === 'isNotNull') return { kind: 'expr', text: `${field}!=null` };\n // Missing / `undefined` value: no constraint was expressed, so drop — the\n // same rule, in the same position, as `@burojs/hasura`'s. Before this, an\n // absent value was stringified into the literal filter `a=\"undefined\"`,\n // which matches rows whose column really contains the text \"undefined\".\n //\n // `''` is deliberately NOT dropped, in either provider. See the matching\n // comment in `@burojs/hasura`'s `filtersToWhere`: the empty-search-box idiom\n // that motivated dropping it is already handled by `searchToFilter`\n // (`@burojs/core`), which never builds a predicate for an empty query, so\n // dropping it again here only cost the ordinary `eq: ''` case.\n if (!('value' in candidate) || value === undefined) return DROP;\n /**\n * The four set-valued operators. PocketBase has no native `in`; each is\n * expanded into an `||`/`&&` chain of comparisons, and an EMPTY chain is not\n * expressible in the grammar at all — which is precisely why the empty case\n * must become a folded constant rather than a dropped node.\n *\n * The two truth values come from set semantics, not from a capture (this\n * repo holds no captured PocketBase responses): `x ∈ ∅` is false, `x ∉ ∅` is\n * true, \"has any of ∅\" is false, and \"has all of ∅\" is vacuously true.\n */\n const chain = (\n join: ' || ' | ' && ',\n term: (entry: unknown) => string,\n empty: boolean,\n ): FilterNode => {\n if (!Array.isArray(value)) return DROP;\n if (value.length === 0) return constant(empty);\n return { kind: 'expr', text: `(${value.map(term).join(join)})` };\n };\n\n switch (op) {\n case 'eq':\n return { kind: 'expr', text: `${field}=${lit(value)}` };\n case 'neq':\n return { kind: 'expr', text: `${field}!=${lit(value)}` };\n case 'contains':\n return { kind: 'expr', text: `${field}~${lit(value)}` };\n case 'startsWith':\n return { kind: 'expr', text: `${field}~${lit(`${String(value)}%`)}` };\n case 'endsWith':\n return { kind: 'expr', text: `${field}~${lit(`%${String(value)}`)}` };\n case 'gt':\n return { kind: 'expr', text: `${field}>${lit(value)}` };\n case 'gte':\n return { kind: 'expr', text: `${field}>=${lit(value)}` };\n case 'lt':\n return { kind: 'expr', text: `${field}<${lit(value)}` };\n case 'lte':\n return { kind: 'expr', text: `${field}<=${lit(value)}` };\n case 'in':\n return chain(' || ', (entry) => `${field}=${lit(entry)}`, false);\n case 'notIn':\n return chain(' && ', (entry) => `${field}!=${lit(entry)}`, true);\n case 'has':\n return { kind: 'expr', text: `${field}~${lit(value)}` };\n case 'hasAny':\n return chain(' || ', (entry) => `${field}~${lit(entry)}`, false);\n case 'hasAll':\n return chain(' && ', (entry) => `${field}~${lit(entry)}`, true);\n // An operator this provider cannot express fails LOUDLY, and with the same\n // `kind` and `code` `@burojs/hasura` uses. It used to return DROP, i.e. \"no\n // constraint\" — so an unknown operator quietly matched every record here\n // while the sibling quietly turned it into `_eq`. Two silent, DIFFERENT\n // wrong answers for one input is the same defect class as the branchless\n // combinators, one hop out.\n default:\n throw unsupportedOperator(field, op);\n }\n}\n\n/**\n * Translate a portable filter expression into a PocketBase `filter` string.\n *\n * `undefined` means **matches every record** — send no `filter` param. It is\n * the answer for both a constraint-free filter and a filter that folds to\n * TRUE; the two are indistinguishable on the wire and there is nothing to\n * gain by distinguishing them.\n *\n * A filter that folds to FALSE returns {@link MATCHES_NO_RECORD} — a real\n * expression that matches nothing — never `undefined`. Returning `undefined`\n * for it is the inversion described on {@link FilterNode}: the caller omits\n * the param and PocketBase answers with every row.\n */\nexport function filterExpression(filter: unknown): string | undefined {\n const node = translateFilter(filter);\n if (node.kind === 'expr') return node.text;\n if (node.kind === 'const') return node.value ? undefined : MATCHES_NO_RECORD;\n return undefined;\n}\n\nfunction recordResult(record: unknown, idField: string) {\n if (record === null || typeof record !== 'object') {\n throw {\n kind: 'protocol',\n code: 'invalid-record',\n message: 'PocketBase returned a non-object record.',\n } satisfies DataError;\n }\n const id = (record as Record<string, unknown>)[idField];\n if (typeof id !== 'string' && (typeof id !== 'number' || !Number.isFinite(id))) {\n throw {\n kind: 'protocol',\n code: 'invalid-record-id',\n message: `PocketBase record is missing a valid ${idField} field.`,\n } satisfies DataError;\n }\n return { kind: 'record' as const, id: id as RecordId, record, completeness: 'full' as const };\n}\n\nexport function createPocketbaseDataProvider(opts: PocketbaseDataProviderOpts): DataProvider {\n const base = opts.baseUrl.replace(/\\/+$/, '');\n const records = (resource: string) => `${base}/api/collections/${resource}/records`;\n\n async function call(url: string, init: RequestInit = {}, signal?: AbortSignal): Promise<unknown> {\n try {\n const token = opts.getToken();\n const headers = new Headers(init.headers);\n if (token) headers.set('authorization', token);\n if (init.body && !(init.body instanceof FormData))\n headers.set('content-type', 'application/json');\n\n const response = await (opts.fetchImpl ?? fetch)(url, { ...init, headers, signal });\n const text = await response.text();\n let json: Record<string, unknown> = {};\n if (text) {\n try {\n json = JSON.parse(text) as Record<string, unknown>;\n } catch {\n throw {\n kind: 'protocol',\n code: 'invalid-json',\n message: 'PocketBase returned invalid JSON.',\n status: response.status,\n } satisfies DataError;\n }\n }\n if (!response.ok) {\n throw {\n kind: 'transport',\n code: 'pocketbase-request-failed',\n message:\n typeof json.message === 'string' ? json.message : `PocketBase error ${response.status}`,\n status: response.status,\n details: json,\n } satisfies DataError;\n }\n return json;\n } catch (error) {\n if (error !== null && typeof error === 'object' && 'kind' in error) throw error;\n throw {\n kind: signal?.aborted ? 'cancelled' : 'transport',\n code: signal?.aborted ? 'request-aborted' : 'pocketbase-request-failed',\n message: error instanceof Error ? error.message : 'PocketBase request failed.',\n cause: error,\n } satisfies DataError;\n }\n }\n\n const body = (input: unknown): BodyInit =>\n input instanceof FormData ? input : JSON.stringify(input);\n\n return defineDataProvider({\n queries: {\n async list(descriptor, context) {\n const params = new URLSearchParams();\n if (descriptor.pagination?.kind === 'offset') {\n params.set('page', String(descriptor.pagination.page));\n params.set('perPage', String(descriptor.pagination.pageSize));\n } else if (descriptor.pagination?.kind === 'cursor') {\n throw {\n kind: 'capability',\n code: 'cursor-pagination-unsupported',\n message: 'PocketBase provider supports offset pagination only.',\n } satisfies DataError;\n }\n // No `filter` param means \"every record\", and that is exactly what an\n // absent `filterExpression` result now means (see its doc comment).\n // A filter that means \"match nothing\" comes back as a real expression,\n // so it is sent rather than silently omitted.\n const filter = filterExpression(descriptor.filter);\n if (filter) params.set('filter', filter);\n if (descriptor.sort?.length) {\n params.set(\n 'sort',\n descriptor.sort\n .map(({ field, direction }) => `${direction === 'desc' ? '-' : '+'}${field}`)\n .join(','),\n );\n }\n const json = (await call(\n `${records(descriptor.resource)}?${params}`,\n {},\n context.signal,\n )) as { items?: unknown[]; totalItems?: number };\n return {\n data: json.items ?? [],\n ...(typeof json.totalItems === 'number' ? { total: json.totalItems } : {}),\n };\n },\n async one(descriptor, context) {\n return {\n data: await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n {},\n context.signal,\n ),\n };\n },\n async many(descriptor, context) {\n if (descriptor.ids.length === 0) return { data: [] };\n const filter = descriptor.ids.map((id) => `id=${lit(id)}`).join(' || ');\n const json = (await call(\n `${records(descriptor.resource)}?filter=${encodeURIComponent(filter)}&perPage=${descriptor.ids.length}`,\n {},\n context.signal,\n )) as { items?: unknown[] };\n return { data: json.items ?? [] };\n },\n },\n mutations: {\n async createOne(descriptor, context) {\n const record = await call(\n records(descriptor.resource),\n { method: 'POST', body: body(descriptor.input) },\n context.signal,\n );\n return recordResult(record, descriptor.idField);\n },\n async updateOne(descriptor, context) {\n const record = await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n { method: 'PATCH', body: body(descriptor.input) },\n context.signal,\n );\n return recordResult(record, descriptor.idField);\n },\n async deleteOne(descriptor, context) {\n await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n { method: 'DELETE' },\n context.signal,\n );\n return { kind: 'identity', id: descriptor.id };\n },\n },\n });\n}\n","import type { AuthProvider, DataError } from '@burojs/core';\n\nexport interface PocketbaseAuthOpts {\n baseUrl: string;\n fetchImpl?: typeof fetch;\n storageKey?: string;\n}\n\ninterface Session {\n token: string;\n record: { id: string; email?: string; name?: string; avatar?: string };\n}\n\n/**\n * Password auth against a PocketBase `users` collection, with the session in\n * localStorage. Deliberately tiny: the example exists to demonstrate the file\n * storage port, not to be an auth reference.\n */\nexport function createPocketbaseAuthProvider(\n opts: PocketbaseAuthOpts,\n): AuthProvider & { getToken(): string | null } {\n const base = opts.baseUrl.replace(/\\/+$/, '');\n const key = opts.storageKey ?? 'buro-example-vite-auth';\n\n const read = (): Session | null => {\n try {\n const raw = localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Partial<Session> | null;\n // The try/catch above only guards JSON.parse throwing — it says nothing\n // about the parsed SHAPE. A value that is valid JSON but not a Session\n // (a stale format from an earlier version, manual tampering) must be\n // treated as \"not logged in\", not crash `getIdentity` reading\n // `record.id` off `undefined`.\n if (!parsed || typeof parsed.token !== 'string' || typeof parsed.record?.id !== 'string') {\n return null;\n }\n return parsed as Session;\n } catch {\n return null;\n }\n };\n\n return {\n getToken: () => read()?.token ?? null,\n\n async login(params: unknown) {\n const { email, password } = params as { email: string; password: string };\n // Resolved HERE, per call — not once at construction. Captured at\n // construction, a `fetch` installed afterwards is unreachable for the life\n // of the provider: a test stub set after the factory ran silently never\n // applies and the call escapes to the real network instead. The sibling\n // `data-provider.ts` carried the identical bug, and it stayed invisible\n // until something needed real fixture data from it.\n const fetchImpl = opts.fetchImpl ?? fetch;\n const res = await fetchImpl(`${base}/api/collections/users/auth-with-password`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ identity: email, password }),\n });\n if (!res.ok) {\n return {\n success: false,\n error: { kind: 'auth', code: 'invalid-credentials', message: 'Invalid credentials' },\n };\n }\n localStorage.setItem(key, JSON.stringify((await res.json()) as Session));\n return { success: true };\n },\n\n async logout() {\n localStorage.removeItem(key);\n return { success: true };\n },\n\n async check() {\n return read() ? { authenticated: true } : { authenticated: false, reason: 'signed-out' };\n },\n\n async onError(error: DataError) {\n return { action: 'propagate', error };\n },\n\n async getIdentity() {\n const session = read();\n if (!session) return null;\n return { id: session.record.id, name: session.record.name ?? session.record.email };\n },\n };\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import { DataProvider, AuthProvider } from '@burojs/core';
2
+
3
+ interface PocketbaseDataProviderOpts {
4
+ baseUrl: string;
5
+ getToken: () => string | null;
6
+ fetchImpl?: typeof fetch;
7
+ }
8
+ /**
9
+ * The shared refusal code for "this provider cannot express that filter
10
+ * operator". `@burojs/hasura` exports the identical string as
11
+ * `FILTER_OPERATOR_UNSUPPORTED_CODE` and throws the same `kind`, so an
12
+ * application that handles one handles the other.
13
+ *
14
+ * The two packages do not import from each other (neither depends on the
15
+ * other, and a shared constant would have to live in `@burojs/core`, which
16
+ * owns the filter AST but not its transports). The agreement is therefore
17
+ * pinned by a test that drives BOTH providers:
18
+ * `examples/marketplace-ops/tests/provider-agreement.test.ts`.
19
+ */
20
+ declare const FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
21
+ /**
22
+ * Translate a portable filter expression into a PocketBase `filter` string.
23
+ *
24
+ * `undefined` means **matches every record** — send no `filter` param. It is
25
+ * the answer for both a constraint-free filter and a filter that folds to
26
+ * TRUE; the two are indistinguishable on the wire and there is nothing to
27
+ * gain by distinguishing them.
28
+ *
29
+ * A filter that folds to FALSE returns {@link MATCHES_NO_RECORD} — a real
30
+ * expression that matches nothing — never `undefined`. Returning `undefined`
31
+ * for it is the inversion described on {@link FilterNode}: the caller omits
32
+ * the param and PocketBase answers with every row.
33
+ */
34
+ declare function filterExpression(filter: unknown): string | undefined;
35
+ declare function createPocketbaseDataProvider(opts: PocketbaseDataProviderOpts): DataProvider;
36
+
37
+ interface PocketbaseAuthOpts {
38
+ baseUrl: string;
39
+ fetchImpl?: typeof fetch;
40
+ storageKey?: string;
41
+ }
42
+ /**
43
+ * Password auth against a PocketBase `users` collection, with the session in
44
+ * localStorage. Deliberately tiny: the example exists to demonstrate the file
45
+ * storage port, not to be an auth reference.
46
+ */
47
+ declare function createPocketbaseAuthProvider(opts: PocketbaseAuthOpts): AuthProvider & {
48
+ getToken(): string | null;
49
+ };
50
+
51
+ export { FILTER_OPERATOR_UNSUPPORTED_CODE, type PocketbaseAuthOpts, type PocketbaseDataProviderOpts, createPocketbaseAuthProvider, createPocketbaseDataProvider, filterExpression };
@@ -0,0 +1,51 @@
1
+ import { DataProvider, AuthProvider } from '@burojs/core';
2
+
3
+ interface PocketbaseDataProviderOpts {
4
+ baseUrl: string;
5
+ getToken: () => string | null;
6
+ fetchImpl?: typeof fetch;
7
+ }
8
+ /**
9
+ * The shared refusal code for "this provider cannot express that filter
10
+ * operator". `@burojs/hasura` exports the identical string as
11
+ * `FILTER_OPERATOR_UNSUPPORTED_CODE` and throws the same `kind`, so an
12
+ * application that handles one handles the other.
13
+ *
14
+ * The two packages do not import from each other (neither depends on the
15
+ * other, and a shared constant would have to live in `@burojs/core`, which
16
+ * owns the filter AST but not its transports). The agreement is therefore
17
+ * pinned by a test that drives BOTH providers:
18
+ * `examples/marketplace-ops/tests/provider-agreement.test.ts`.
19
+ */
20
+ declare const FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
21
+ /**
22
+ * Translate a portable filter expression into a PocketBase `filter` string.
23
+ *
24
+ * `undefined` means **matches every record** — send no `filter` param. It is
25
+ * the answer for both a constraint-free filter and a filter that folds to
26
+ * TRUE; the two are indistinguishable on the wire and there is nothing to
27
+ * gain by distinguishing them.
28
+ *
29
+ * A filter that folds to FALSE returns {@link MATCHES_NO_RECORD} — a real
30
+ * expression that matches nothing — never `undefined`. Returning `undefined`
31
+ * for it is the inversion described on {@link FilterNode}: the caller omits
32
+ * the param and PocketBase answers with every row.
33
+ */
34
+ declare function filterExpression(filter: unknown): string | undefined;
35
+ declare function createPocketbaseDataProvider(opts: PocketbaseDataProviderOpts): DataProvider;
36
+
37
+ interface PocketbaseAuthOpts {
38
+ baseUrl: string;
39
+ fetchImpl?: typeof fetch;
40
+ storageKey?: string;
41
+ }
42
+ /**
43
+ * Password auth against a PocketBase `users` collection, with the session in
44
+ * localStorage. Deliberately tiny: the example exists to demonstrate the file
45
+ * storage port, not to be an auth reference.
46
+ */
47
+ declare function createPocketbaseAuthProvider(opts: PocketbaseAuthOpts): AuthProvider & {
48
+ getToken(): string | null;
49
+ };
50
+
51
+ export { FILTER_OPERATOR_UNSUPPORTED_CODE, type PocketbaseAuthOpts, type PocketbaseDataProviderOpts, createPocketbaseAuthProvider, createPocketbaseDataProvider, filterExpression };
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ import { defineDataProvider } from '@burojs/core';
2
+
3
+ // src/data-provider.ts
4
+ function lit(value) {
5
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
6
+ }
7
+ var FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
8
+ function unsupportedOperator(field, op) {
9
+ return {
10
+ kind: "capability",
11
+ code: FILTER_OPERATOR_UNSUPPORTED_CODE,
12
+ message: `@burojs/pocketbase does not support the filter operator '${op}' (on '${field}').`
13
+ };
14
+ }
15
+ var DROP = { kind: "drop" };
16
+ var constant = (value) => ({ kind: "const", value });
17
+ var MATCHES_NO_RECORD = '(id="" && id!="")';
18
+ function combine(branches, op) {
19
+ if (branches.length === 0) return constant(op === "and");
20
+ const absorbing = op === "or";
21
+ const nodes = branches.map(translateFilter);
22
+ if (nodes.some((node) => node.kind === "const" && node.value === absorbing)) {
23
+ return constant(absorbing);
24
+ }
25
+ const parts = nodes.flatMap((node) => node.kind === "expr" ? [node.text] : []);
26
+ if (parts.length > 0)
27
+ return { kind: "expr", text: `(${parts.join(op === "and" ? " && " : " || ")})` };
28
+ return nodes.some((node) => node.kind === "const") ? constant(!absorbing) : DROP;
29
+ }
30
+ function translateFilter(filter) {
31
+ if (filter === null || typeof filter !== "object") return DROP;
32
+ const candidate = filter;
33
+ if (Array.isArray(candidate.and)) return combine(candidate.and, "and");
34
+ if (Array.isArray(candidate.or)) return combine(candidate.or, "or");
35
+ if ("not" in candidate) {
36
+ const inner = translateFilter(candidate.not);
37
+ if (inner.kind === "drop") return DROP;
38
+ if (inner.kind === "const") return constant(!inner.value);
39
+ return { kind: "expr", text: `!(${inner.text})` };
40
+ }
41
+ if (typeof candidate.field !== "string" || typeof candidate.op !== "string") return DROP;
42
+ const { field, op, value } = candidate;
43
+ if (op === "isNull") return { kind: "expr", text: `${field}=null` };
44
+ if (op === "isNotNull") return { kind: "expr", text: `${field}!=null` };
45
+ if (!("value" in candidate) || value === void 0) return DROP;
46
+ const chain = (join, term, empty) => {
47
+ if (!Array.isArray(value)) return DROP;
48
+ if (value.length === 0) return constant(empty);
49
+ return { kind: "expr", text: `(${value.map(term).join(join)})` };
50
+ };
51
+ switch (op) {
52
+ case "eq":
53
+ return { kind: "expr", text: `${field}=${lit(value)}` };
54
+ case "neq":
55
+ return { kind: "expr", text: `${field}!=${lit(value)}` };
56
+ case "contains":
57
+ return { kind: "expr", text: `${field}~${lit(value)}` };
58
+ case "startsWith":
59
+ return { kind: "expr", text: `${field}~${lit(`${String(value)}%`)}` };
60
+ case "endsWith":
61
+ return { kind: "expr", text: `${field}~${lit(`%${String(value)}`)}` };
62
+ case "gt":
63
+ return { kind: "expr", text: `${field}>${lit(value)}` };
64
+ case "gte":
65
+ return { kind: "expr", text: `${field}>=${lit(value)}` };
66
+ case "lt":
67
+ return { kind: "expr", text: `${field}<${lit(value)}` };
68
+ case "lte":
69
+ return { kind: "expr", text: `${field}<=${lit(value)}` };
70
+ case "in":
71
+ return chain(" || ", (entry) => `${field}=${lit(entry)}`, false);
72
+ case "notIn":
73
+ return chain(" && ", (entry) => `${field}!=${lit(entry)}`, true);
74
+ case "has":
75
+ return { kind: "expr", text: `${field}~${lit(value)}` };
76
+ case "hasAny":
77
+ return chain(" || ", (entry) => `${field}~${lit(entry)}`, false);
78
+ case "hasAll":
79
+ return chain(" && ", (entry) => `${field}~${lit(entry)}`, true);
80
+ // An operator this provider cannot express fails LOUDLY, and with the same
81
+ // `kind` and `code` `@burojs/hasura` uses. It used to return DROP, i.e. "no
82
+ // constraint" — so an unknown operator quietly matched every record here
83
+ // while the sibling quietly turned it into `_eq`. Two silent, DIFFERENT
84
+ // wrong answers for one input is the same defect class as the branchless
85
+ // combinators, one hop out.
86
+ default:
87
+ throw unsupportedOperator(field, op);
88
+ }
89
+ }
90
+ function filterExpression(filter) {
91
+ const node = translateFilter(filter);
92
+ if (node.kind === "expr") return node.text;
93
+ if (node.kind === "const") return node.value ? void 0 : MATCHES_NO_RECORD;
94
+ return void 0;
95
+ }
96
+ function recordResult(record, idField) {
97
+ if (record === null || typeof record !== "object") {
98
+ throw {
99
+ kind: "protocol",
100
+ code: "invalid-record",
101
+ message: "PocketBase returned a non-object record."
102
+ };
103
+ }
104
+ const id = record[idField];
105
+ if (typeof id !== "string" && (typeof id !== "number" || !Number.isFinite(id))) {
106
+ throw {
107
+ kind: "protocol",
108
+ code: "invalid-record-id",
109
+ message: `PocketBase record is missing a valid ${idField} field.`
110
+ };
111
+ }
112
+ return { kind: "record", id, record, completeness: "full" };
113
+ }
114
+ function createPocketbaseDataProvider(opts) {
115
+ const base = opts.baseUrl.replace(/\/+$/, "");
116
+ const records = (resource) => `${base}/api/collections/${resource}/records`;
117
+ async function call(url, init = {}, signal) {
118
+ try {
119
+ const token = opts.getToken();
120
+ const headers = new Headers(init.headers);
121
+ if (token) headers.set("authorization", token);
122
+ if (init.body && !(init.body instanceof FormData))
123
+ headers.set("content-type", "application/json");
124
+ const response = await (opts.fetchImpl ?? fetch)(url, { ...init, headers, signal });
125
+ const text = await response.text();
126
+ let json = {};
127
+ if (text) {
128
+ try {
129
+ json = JSON.parse(text);
130
+ } catch {
131
+ throw {
132
+ kind: "protocol",
133
+ code: "invalid-json",
134
+ message: "PocketBase returned invalid JSON.",
135
+ status: response.status
136
+ };
137
+ }
138
+ }
139
+ if (!response.ok) {
140
+ throw {
141
+ kind: "transport",
142
+ code: "pocketbase-request-failed",
143
+ message: typeof json.message === "string" ? json.message : `PocketBase error ${response.status}`,
144
+ status: response.status,
145
+ details: json
146
+ };
147
+ }
148
+ return json;
149
+ } catch (error) {
150
+ if (error !== null && typeof error === "object" && "kind" in error) throw error;
151
+ throw {
152
+ kind: signal?.aborted ? "cancelled" : "transport",
153
+ code: signal?.aborted ? "request-aborted" : "pocketbase-request-failed",
154
+ message: error instanceof Error ? error.message : "PocketBase request failed.",
155
+ cause: error
156
+ };
157
+ }
158
+ }
159
+ const body = (input) => input instanceof FormData ? input : JSON.stringify(input);
160
+ return defineDataProvider({
161
+ queries: {
162
+ async list(descriptor, context) {
163
+ const params = new URLSearchParams();
164
+ if (descriptor.pagination?.kind === "offset") {
165
+ params.set("page", String(descriptor.pagination.page));
166
+ params.set("perPage", String(descriptor.pagination.pageSize));
167
+ } else if (descriptor.pagination?.kind === "cursor") {
168
+ throw {
169
+ kind: "capability",
170
+ code: "cursor-pagination-unsupported",
171
+ message: "PocketBase provider supports offset pagination only."
172
+ };
173
+ }
174
+ const filter = filterExpression(descriptor.filter);
175
+ if (filter) params.set("filter", filter);
176
+ if (descriptor.sort?.length) {
177
+ params.set(
178
+ "sort",
179
+ descriptor.sort.map(({ field, direction }) => `${direction === "desc" ? "-" : "+"}${field}`).join(",")
180
+ );
181
+ }
182
+ const json = await call(
183
+ `${records(descriptor.resource)}?${params}`,
184
+ {},
185
+ context.signal
186
+ );
187
+ return {
188
+ data: json.items ?? [],
189
+ ...typeof json.totalItems === "number" ? { total: json.totalItems } : {}
190
+ };
191
+ },
192
+ async one(descriptor, context) {
193
+ return {
194
+ data: await call(
195
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
196
+ {},
197
+ context.signal
198
+ )
199
+ };
200
+ },
201
+ async many(descriptor, context) {
202
+ if (descriptor.ids.length === 0) return { data: [] };
203
+ const filter = descriptor.ids.map((id) => `id=${lit(id)}`).join(" || ");
204
+ const json = await call(
205
+ `${records(descriptor.resource)}?filter=${encodeURIComponent(filter)}&perPage=${descriptor.ids.length}`,
206
+ {},
207
+ context.signal
208
+ );
209
+ return { data: json.items ?? [] };
210
+ }
211
+ },
212
+ mutations: {
213
+ async createOne(descriptor, context) {
214
+ const record = await call(
215
+ records(descriptor.resource),
216
+ { method: "POST", body: body(descriptor.input) },
217
+ context.signal
218
+ );
219
+ return recordResult(record, descriptor.idField);
220
+ },
221
+ async updateOne(descriptor, context) {
222
+ const record = await call(
223
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
224
+ { method: "PATCH", body: body(descriptor.input) },
225
+ context.signal
226
+ );
227
+ return recordResult(record, descriptor.idField);
228
+ },
229
+ async deleteOne(descriptor, context) {
230
+ await call(
231
+ `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,
232
+ { method: "DELETE" },
233
+ context.signal
234
+ );
235
+ return { kind: "identity", id: descriptor.id };
236
+ }
237
+ }
238
+ });
239
+ }
240
+
241
+ // src/auth-provider.ts
242
+ function createPocketbaseAuthProvider(opts) {
243
+ const base = opts.baseUrl.replace(/\/+$/, "");
244
+ const key = opts.storageKey ?? "buro-example-vite-auth";
245
+ const read = () => {
246
+ try {
247
+ const raw = localStorage.getItem(key);
248
+ if (!raw) return null;
249
+ const parsed = JSON.parse(raw);
250
+ if (!parsed || typeof parsed.token !== "string" || typeof parsed.record?.id !== "string") {
251
+ return null;
252
+ }
253
+ return parsed;
254
+ } catch {
255
+ return null;
256
+ }
257
+ };
258
+ return {
259
+ getToken: () => read()?.token ?? null,
260
+ async login(params) {
261
+ const { email, password } = params;
262
+ const fetchImpl = opts.fetchImpl ?? fetch;
263
+ const res = await fetchImpl(`${base}/api/collections/users/auth-with-password`, {
264
+ method: "POST",
265
+ headers: { "content-type": "application/json" },
266
+ body: JSON.stringify({ identity: email, password })
267
+ });
268
+ if (!res.ok) {
269
+ return {
270
+ success: false,
271
+ error: { kind: "auth", code: "invalid-credentials", message: "Invalid credentials" }
272
+ };
273
+ }
274
+ localStorage.setItem(key, JSON.stringify(await res.json()));
275
+ return { success: true };
276
+ },
277
+ async logout() {
278
+ localStorage.removeItem(key);
279
+ return { success: true };
280
+ },
281
+ async check() {
282
+ return read() ? { authenticated: true } : { authenticated: false, reason: "signed-out" };
283
+ },
284
+ async onError(error) {
285
+ return { action: "propagate", error };
286
+ },
287
+ async getIdentity() {
288
+ const session = read();
289
+ if (!session) return null;
290
+ return { id: session.record.id, name: session.record.name ?? session.record.email };
291
+ }
292
+ };
293
+ }
294
+
295
+ export { FILTER_OPERATOR_UNSUPPORTED_CODE, createPocketbaseAuthProvider, createPocketbaseDataProvider, filterExpression };
296
+ //# sourceMappingURL=index.js.map
297
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-provider.ts","../src/auth-provider.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { defineDataProvider, type DataError, type DataProvider, type RecordId } from '@burojs/core';\n\nexport interface PocketbaseDataProviderOpts {\n baseUrl: string;\n getToken: () => string | null;\n fetchImpl?: typeof fetch;\n}\n\n/** Escapes a value for a PocketBase filter string literal. */\nfunction lit(value: unknown): string {\n return `\"${String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * The shared refusal code for \"this provider cannot express that filter\n * operator\". `@burojs/hasura` exports the identical string as\n * `FILTER_OPERATOR_UNSUPPORTED_CODE` and throws the same `kind`, so an\n * application that handles one handles the other.\n *\n * The two packages do not import from each other (neither depends on the\n * other, and a shared constant would have to live in `@burojs/core`, which\n * owns the filter AST but not its transports). The agreement is therefore\n * pinned by a test that drives BOTH providers:\n * `examples/marketplace-ops/tests/provider-agreement.test.ts`.\n */\nexport const FILTER_OPERATOR_UNSUPPORTED_CODE = 'filter-operator-unsupported';\n\nfunction unsupportedOperator(field: string, op: string): DataError {\n return {\n kind: 'capability',\n code: FILTER_OPERATOR_UNSUPPORTED_CODE,\n message: `@burojs/pocketbase does not support the filter operator '${op}' (on '${field}').`,\n };\n}\n\n/**\n * What one filter node translated to. Three outcomes, not two — that missing\n * third outcome IS the defect this type exists to make unrepresentable.\n *\n * THE CLASS, stated once. The old signature was `string | undefined`, and\n * `undefined` carried two incompatible meanings at once:\n *\n * - \"no constraint was expressed here — drop me\" (no value supplied, an\n * unrecognised node), and\n * - \"this node is a constant\", which is what a BRANCHLESS COMBINATOR is:\n * `{and: []}` is vacuously TRUE, `{or: []}` is vacuously FALSE, `in: []`\n * is an empty-set membership test and therefore FALSE.\n *\n * Dropping a node is sound only when the node is TRUE and sits in an AND\n * position, where a TRUE conjunct is genuinely absorbed. Everywhere else —\n * under `not`, beside a disjunct in an `or`, or alone at the top level, where\n * the caller turns `undefined` into no `filter` param at all and the server\n * answers with EVERY row — dropping a false node inverts the query.\n *\n * The sibling `@burojs/hasura` closed the same class in `filtersToWhere` by\n * emitting `{_and: []}` / `{_or: []}` explicitly, because Hasura's `bool_exp`\n * has a literal for both truth values. PocketBase's filter grammar has none\n * (every term is `field op value`), so this half CANNOT emit a truth value as\n * a term; it folds instead and materialises only the one constant it cannot\n * fold away — see {@link MATCHES_NO_RECORD}.\n */\ntype FilterNode =\n | { readonly kind: 'expr'; readonly text: string }\n | { readonly kind: 'const'; readonly value: boolean }\n | { readonly kind: 'drop' };\n\nconst DROP: FilterNode = { kind: 'drop' };\nconst constant = (value: boolean): FilterNode => ({ kind: 'const', value });\n\n/**\n * A PocketBase filter expression that matches NO record.\n *\n * It has to be a real expression: PocketBase's filter grammar has no boolean\n * literal and no nullary operator — every term is `field op value`, combined\n * with `&&`, `||`, `!(…)` and parentheses (the grammar `@burojs/mock-kit`'s\n * `parsePbFilter` implements, and the one this file's own emitters produce).\n * So \"match nothing\" is spelled as a CONTRADICTION over a column that every\n * record has.\n *\n * Why this particular one: `id=\"\" && id!=\"\"` is false for every possible value\n * of `id`, by the definition of `=` / `!=` alone. It does not depend on what\n * `id` contains, on the collection's schema, or on PocketBase's own guarantee\n * that a record id is a non-empty 15-character string — that guarantee would\n * make the shorter `id=\"\"` sufficient, but the contradiction needs no such\n * premise and stays false under SQL three-valued logic too (a NULL comparison\n * is not TRUE, so the row is still excluded).\n *\n * There is no TRUE counterpart because none is ever needed: a TRUE node is\n * absorbed by every AND, collapses every OR to TRUE, and at the top level is\n * exactly \"send no `filter` param\", which already means every row.\n */\nconst MATCHES_NO_RECORD = '(id=\"\" && id!=\"\")';\n\n/**\n * Fold a combinator's branches. `op` supplies its own absorbing element\n * (FALSE for `and`, TRUE for `or`) and identity element (the other one).\n *\n * The branchless case is checked on the SOURCE array, before mapping, and is\n * deliberately NOT the same case as \"every branch mapped away because no VALUE\n * was supplied\". That second case still yields `drop`, and `@burojs/hasura`\n * drops on exactly the same condition — a missing or `undefined` value, never\n * on `''`, which both providers translate.\n */\nfunction combine(branches: readonly unknown[], op: 'and' | 'or'): FilterNode {\n if (branches.length === 0) return constant(op === 'and');\n const absorbing = op === 'or';\n const nodes = branches.map(translateFilter);\n if (nodes.some((node) => node.kind === 'const' && node.value === absorbing)) {\n return constant(absorbing);\n }\n const parts = nodes.flatMap((node) => (node.kind === 'expr' ? [node.text] : []));\n if (parts.length > 0)\n return { kind: 'expr', text: `(${parts.join(op === 'and' ? ' && ' : ' || ')})` };\n // Nothing left to write down. A surviving identity-element constant still\n // has to be reported as a constant (it may sit under a `not`); if there were\n // no constants at all, every branch was a drop and so is the combinator.\n return nodes.some((node) => node.kind === 'const') ? constant(!absorbing) : DROP;\n}\n\nfunction translateFilter(filter: unknown): FilterNode {\n if (filter === null || typeof filter !== 'object') return DROP;\n const candidate = filter as Record<string, unknown>;\n if (Array.isArray(candidate.and)) return combine(candidate.and, 'and');\n if (Array.isArray(candidate.or)) return combine(candidate.or, 'or');\n if ('not' in candidate) {\n const inner = translateFilter(candidate.not);\n if (inner.kind === 'drop') return DROP;\n if (inner.kind === 'const') return constant(!inner.value);\n return { kind: 'expr', text: `!(${inner.text})` };\n }\n if (typeof candidate.field !== 'string' || typeof candidate.op !== 'string') return DROP;\n\n const { field, op, value } = candidate;\n // `isNull`/`isNotNull` carry no value and are answered before the value\n // check, exactly as `@burojs/hasura` orders it.\n if (op === 'isNull') return { kind: 'expr', text: `${field}=null` };\n if (op === 'isNotNull') return { kind: 'expr', text: `${field}!=null` };\n // Missing / `undefined` value: no constraint was expressed, so drop — the\n // same rule, in the same position, as `@burojs/hasura`'s. Before this, an\n // absent value was stringified into the literal filter `a=\"undefined\"`,\n // which matches rows whose column really contains the text \"undefined\".\n //\n // `''` is deliberately NOT dropped, in either provider. See the matching\n // comment in `@burojs/hasura`'s `filtersToWhere`: the empty-search-box idiom\n // that motivated dropping it is already handled by `searchToFilter`\n // (`@burojs/core`), which never builds a predicate for an empty query, so\n // dropping it again here only cost the ordinary `eq: ''` case.\n if (!('value' in candidate) || value === undefined) return DROP;\n /**\n * The four set-valued operators. PocketBase has no native `in`; each is\n * expanded into an `||`/`&&` chain of comparisons, and an EMPTY chain is not\n * expressible in the grammar at all — which is precisely why the empty case\n * must become a folded constant rather than a dropped node.\n *\n * The two truth values come from set semantics, not from a capture (this\n * repo holds no captured PocketBase responses): `x ∈ ∅` is false, `x ∉ ∅` is\n * true, \"has any of ∅\" is false, and \"has all of ∅\" is vacuously true.\n */\n const chain = (\n join: ' || ' | ' && ',\n term: (entry: unknown) => string,\n empty: boolean,\n ): FilterNode => {\n if (!Array.isArray(value)) return DROP;\n if (value.length === 0) return constant(empty);\n return { kind: 'expr', text: `(${value.map(term).join(join)})` };\n };\n\n switch (op) {\n case 'eq':\n return { kind: 'expr', text: `${field}=${lit(value)}` };\n case 'neq':\n return { kind: 'expr', text: `${field}!=${lit(value)}` };\n case 'contains':\n return { kind: 'expr', text: `${field}~${lit(value)}` };\n case 'startsWith':\n return { kind: 'expr', text: `${field}~${lit(`${String(value)}%`)}` };\n case 'endsWith':\n return { kind: 'expr', text: `${field}~${lit(`%${String(value)}`)}` };\n case 'gt':\n return { kind: 'expr', text: `${field}>${lit(value)}` };\n case 'gte':\n return { kind: 'expr', text: `${field}>=${lit(value)}` };\n case 'lt':\n return { kind: 'expr', text: `${field}<${lit(value)}` };\n case 'lte':\n return { kind: 'expr', text: `${field}<=${lit(value)}` };\n case 'in':\n return chain(' || ', (entry) => `${field}=${lit(entry)}`, false);\n case 'notIn':\n return chain(' && ', (entry) => `${field}!=${lit(entry)}`, true);\n case 'has':\n return { kind: 'expr', text: `${field}~${lit(value)}` };\n case 'hasAny':\n return chain(' || ', (entry) => `${field}~${lit(entry)}`, false);\n case 'hasAll':\n return chain(' && ', (entry) => `${field}~${lit(entry)}`, true);\n // An operator this provider cannot express fails LOUDLY, and with the same\n // `kind` and `code` `@burojs/hasura` uses. It used to return DROP, i.e. \"no\n // constraint\" — so an unknown operator quietly matched every record here\n // while the sibling quietly turned it into `_eq`. Two silent, DIFFERENT\n // wrong answers for one input is the same defect class as the branchless\n // combinators, one hop out.\n default:\n throw unsupportedOperator(field, op);\n }\n}\n\n/**\n * Translate a portable filter expression into a PocketBase `filter` string.\n *\n * `undefined` means **matches every record** — send no `filter` param. It is\n * the answer for both a constraint-free filter and a filter that folds to\n * TRUE; the two are indistinguishable on the wire and there is nothing to\n * gain by distinguishing them.\n *\n * A filter that folds to FALSE returns {@link MATCHES_NO_RECORD} — a real\n * expression that matches nothing — never `undefined`. Returning `undefined`\n * for it is the inversion described on {@link FilterNode}: the caller omits\n * the param and PocketBase answers with every row.\n */\nexport function filterExpression(filter: unknown): string | undefined {\n const node = translateFilter(filter);\n if (node.kind === 'expr') return node.text;\n if (node.kind === 'const') return node.value ? undefined : MATCHES_NO_RECORD;\n return undefined;\n}\n\nfunction recordResult(record: unknown, idField: string) {\n if (record === null || typeof record !== 'object') {\n throw {\n kind: 'protocol',\n code: 'invalid-record',\n message: 'PocketBase returned a non-object record.',\n } satisfies DataError;\n }\n const id = (record as Record<string, unknown>)[idField];\n if (typeof id !== 'string' && (typeof id !== 'number' || !Number.isFinite(id))) {\n throw {\n kind: 'protocol',\n code: 'invalid-record-id',\n message: `PocketBase record is missing a valid ${idField} field.`,\n } satisfies DataError;\n }\n return { kind: 'record' as const, id: id as RecordId, record, completeness: 'full' as const };\n}\n\nexport function createPocketbaseDataProvider(opts: PocketbaseDataProviderOpts): DataProvider {\n const base = opts.baseUrl.replace(/\\/+$/, '');\n const records = (resource: string) => `${base}/api/collections/${resource}/records`;\n\n async function call(url: string, init: RequestInit = {}, signal?: AbortSignal): Promise<unknown> {\n try {\n const token = opts.getToken();\n const headers = new Headers(init.headers);\n if (token) headers.set('authorization', token);\n if (init.body && !(init.body instanceof FormData))\n headers.set('content-type', 'application/json');\n\n const response = await (opts.fetchImpl ?? fetch)(url, { ...init, headers, signal });\n const text = await response.text();\n let json: Record<string, unknown> = {};\n if (text) {\n try {\n json = JSON.parse(text) as Record<string, unknown>;\n } catch {\n throw {\n kind: 'protocol',\n code: 'invalid-json',\n message: 'PocketBase returned invalid JSON.',\n status: response.status,\n } satisfies DataError;\n }\n }\n if (!response.ok) {\n throw {\n kind: 'transport',\n code: 'pocketbase-request-failed',\n message:\n typeof json.message === 'string' ? json.message : `PocketBase error ${response.status}`,\n status: response.status,\n details: json,\n } satisfies DataError;\n }\n return json;\n } catch (error) {\n if (error !== null && typeof error === 'object' && 'kind' in error) throw error;\n throw {\n kind: signal?.aborted ? 'cancelled' : 'transport',\n code: signal?.aborted ? 'request-aborted' : 'pocketbase-request-failed',\n message: error instanceof Error ? error.message : 'PocketBase request failed.',\n cause: error,\n } satisfies DataError;\n }\n }\n\n const body = (input: unknown): BodyInit =>\n input instanceof FormData ? input : JSON.stringify(input);\n\n return defineDataProvider({\n queries: {\n async list(descriptor, context) {\n const params = new URLSearchParams();\n if (descriptor.pagination?.kind === 'offset') {\n params.set('page', String(descriptor.pagination.page));\n params.set('perPage', String(descriptor.pagination.pageSize));\n } else if (descriptor.pagination?.kind === 'cursor') {\n throw {\n kind: 'capability',\n code: 'cursor-pagination-unsupported',\n message: 'PocketBase provider supports offset pagination only.',\n } satisfies DataError;\n }\n // No `filter` param means \"every record\", and that is exactly what an\n // absent `filterExpression` result now means (see its doc comment).\n // A filter that means \"match nothing\" comes back as a real expression,\n // so it is sent rather than silently omitted.\n const filter = filterExpression(descriptor.filter);\n if (filter) params.set('filter', filter);\n if (descriptor.sort?.length) {\n params.set(\n 'sort',\n descriptor.sort\n .map(({ field, direction }) => `${direction === 'desc' ? '-' : '+'}${field}`)\n .join(','),\n );\n }\n const json = (await call(\n `${records(descriptor.resource)}?${params}`,\n {},\n context.signal,\n )) as { items?: unknown[]; totalItems?: number };\n return {\n data: json.items ?? [],\n ...(typeof json.totalItems === 'number' ? { total: json.totalItems } : {}),\n };\n },\n async one(descriptor, context) {\n return {\n data: await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n {},\n context.signal,\n ),\n };\n },\n async many(descriptor, context) {\n if (descriptor.ids.length === 0) return { data: [] };\n const filter = descriptor.ids.map((id) => `id=${lit(id)}`).join(' || ');\n const json = (await call(\n `${records(descriptor.resource)}?filter=${encodeURIComponent(filter)}&perPage=${descriptor.ids.length}`,\n {},\n context.signal,\n )) as { items?: unknown[] };\n return { data: json.items ?? [] };\n },\n },\n mutations: {\n async createOne(descriptor, context) {\n const record = await call(\n records(descriptor.resource),\n { method: 'POST', body: body(descriptor.input) },\n context.signal,\n );\n return recordResult(record, descriptor.idField);\n },\n async updateOne(descriptor, context) {\n const record = await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n { method: 'PATCH', body: body(descriptor.input) },\n context.signal,\n );\n return recordResult(record, descriptor.idField);\n },\n async deleteOne(descriptor, context) {\n await call(\n `${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`,\n { method: 'DELETE' },\n context.signal,\n );\n return { kind: 'identity', id: descriptor.id };\n },\n },\n });\n}\n","import type { AuthProvider, DataError } from '@burojs/core';\n\nexport interface PocketbaseAuthOpts {\n baseUrl: string;\n fetchImpl?: typeof fetch;\n storageKey?: string;\n}\n\ninterface Session {\n token: string;\n record: { id: string; email?: string; name?: string; avatar?: string };\n}\n\n/**\n * Password auth against a PocketBase `users` collection, with the session in\n * localStorage. Deliberately tiny: the example exists to demonstrate the file\n * storage port, not to be an auth reference.\n */\nexport function createPocketbaseAuthProvider(\n opts: PocketbaseAuthOpts,\n): AuthProvider & { getToken(): string | null } {\n const base = opts.baseUrl.replace(/\\/+$/, '');\n const key = opts.storageKey ?? 'buro-example-vite-auth';\n\n const read = (): Session | null => {\n try {\n const raw = localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Partial<Session> | null;\n // The try/catch above only guards JSON.parse throwing — it says nothing\n // about the parsed SHAPE. A value that is valid JSON but not a Session\n // (a stale format from an earlier version, manual tampering) must be\n // treated as \"not logged in\", not crash `getIdentity` reading\n // `record.id` off `undefined`.\n if (!parsed || typeof parsed.token !== 'string' || typeof parsed.record?.id !== 'string') {\n return null;\n }\n return parsed as Session;\n } catch {\n return null;\n }\n };\n\n return {\n getToken: () => read()?.token ?? null,\n\n async login(params: unknown) {\n const { email, password } = params as { email: string; password: string };\n // Resolved HERE, per call — not once at construction. Captured at\n // construction, a `fetch` installed afterwards is unreachable for the life\n // of the provider: a test stub set after the factory ran silently never\n // applies and the call escapes to the real network instead. The sibling\n // `data-provider.ts` carried the identical bug, and it stayed invisible\n // until something needed real fixture data from it.\n const fetchImpl = opts.fetchImpl ?? fetch;\n const res = await fetchImpl(`${base}/api/collections/users/auth-with-password`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ identity: email, password }),\n });\n if (!res.ok) {\n return {\n success: false,\n error: { kind: 'auth', code: 'invalid-credentials', message: 'Invalid credentials' },\n };\n }\n localStorage.setItem(key, JSON.stringify((await res.json()) as Session));\n return { success: true };\n },\n\n async logout() {\n localStorage.removeItem(key);\n return { success: true };\n },\n\n async check() {\n return read() ? { authenticated: true } : { authenticated: false, reason: 'signed-out' };\n },\n\n async onError(error: DataError) {\n return { action: 'propagate', error };\n },\n\n async getIdentity() {\n const session = read();\n if (!session) return null;\n return { id: session.record.id, name: session.record.name ?? session.record.email };\n },\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@burojs/pocketbase",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Buro: PocketBase data provider and auth provider",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@burojs/core": "0.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "jsdom": "^26.0.0",
32
+ "tsup": "^8.3.0",
33
+ "typescript": "^5.5.0",
34
+ "vitest": "^2.1.0",
35
+ "@burojs/tooling-eslint": "0.0.0",
36
+ "@burojs/tooling-tsconfig": "0.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "typecheck:tests": "tsc -p tsconfig.test.json"
46
+ }
47
+ }