@burojs/pocketbase 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,302 +1,312 @@
1
- 'use strict';
2
-
3
- var core = require('@burojs/core');
4
-
5
- // src/data-provider.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _burojs_core = require("@burojs/core");
3
+ //#region src/data-provider.ts
6
4
  function lit(value) {
7
- return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
5
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
8
6
  }
9
- var FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
7
+ const FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
10
8
  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
- };
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
+ };
16
14
  }
17
- var DROP = { kind: "drop" };
18
- var constant = (value) => ({ kind: "const", value });
19
- var MATCHES_NO_RECORD = '(id="" && id!="")';
15
+ const DROP = { kind: "drop" };
16
+ const constant = (value) => ({
17
+ kind: "const",
18
+ value
19
+ });
20
+ const MATCHES_NO_RECORD = "(id=\"\" && id!=\"\")";
20
21
  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;
22
+ if (branches.length === 0) return constant(op === "and");
23
+ const absorbing = op === "or";
24
+ const nodes = branches.map(translateFilter);
25
+ if (nodes.some((node) => node.kind === "const" && node.value === absorbing)) return constant(absorbing);
26
+ const parts = nodes.flatMap((node) => node.kind === "expr" ? [node.text] : []);
27
+ if (parts.length > 0) return {
28
+ kind: "expr",
29
+ text: `(${parts.join(op === "and" ? " && " : " || ")})`
30
+ };
31
+ return nodes.some((node) => node.kind === "const") ? constant(!absorbing) : DROP;
31
32
  }
32
33
  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
- }
34
+ if (filter === null || typeof filter !== "object") return DROP;
35
+ const candidate = filter;
36
+ if (Array.isArray(candidate.and)) return combine(candidate.and, "and");
37
+ if (Array.isArray(candidate.or)) return combine(candidate.or, "or");
38
+ if ("not" in candidate) {
39
+ const inner = translateFilter(candidate.not);
40
+ if (inner.kind === "drop") return DROP;
41
+ if (inner.kind === "const") return constant(!inner.value);
42
+ return {
43
+ kind: "expr",
44
+ text: `!(${inner.text})`
45
+ };
46
+ }
47
+ if (typeof candidate.field !== "string" || typeof candidate.op !== "string") return DROP;
48
+ const { field, op, value } = candidate;
49
+ if (field.includes(".")) throw {
50
+ kind: "capability",
51
+ code: FILTER_OPERATOR_UNSUPPORTED_CODE,
52
+ message: `@burojs/pocketbase cannot express a filter across a relation ('${field}'): PocketBase's traversal syntax depends on the relation's kind and direction, which nothing on the descriptor declares. Filter on a field of this collection.`
53
+ };
54
+ if (op === "isNull") return {
55
+ kind: "expr",
56
+ text: `${field}=null`
57
+ };
58
+ if (op === "isNotNull") return {
59
+ kind: "expr",
60
+ text: `${field}!=null`
61
+ };
62
+ if (!("value" in candidate) || value === void 0) return DROP;
63
+ const chain = (join, term, empty) => {
64
+ if (!Array.isArray(value)) return DROP;
65
+ if (value.length === 0) return constant(empty);
66
+ return {
67
+ kind: "expr",
68
+ text: `(${value.map(term).join(join)})`
69
+ };
70
+ };
71
+ switch (op) {
72
+ case "eq": return {
73
+ kind: "expr",
74
+ text: `${field}=${lit(value)}`
75
+ };
76
+ case "neq": return {
77
+ kind: "expr",
78
+ text: `${field}!=${lit(value)}`
79
+ };
80
+ case "contains": return {
81
+ kind: "expr",
82
+ text: `${field}~${lit(value)}`
83
+ };
84
+ case "startsWith": return {
85
+ kind: "expr",
86
+ text: `${field}~${lit(`${String(value)}%`)}`
87
+ };
88
+ case "endsWith": return {
89
+ kind: "expr",
90
+ text: `${field}~${lit(`%${String(value)}`)}`
91
+ };
92
+ case "gt": return {
93
+ kind: "expr",
94
+ text: `${field}>${lit(value)}`
95
+ };
96
+ case "gte": return {
97
+ kind: "expr",
98
+ text: `${field}>=${lit(value)}`
99
+ };
100
+ case "lt": return {
101
+ kind: "expr",
102
+ text: `${field}<${lit(value)}`
103
+ };
104
+ case "lte": return {
105
+ kind: "expr",
106
+ text: `${field}<=${lit(value)}`
107
+ };
108
+ case "in": return chain(" || ", (entry) => `${field}=${lit(entry)}`, false);
109
+ case "notIn": return chain(" && ", (entry) => `${field}!=${lit(entry)}`, true);
110
+ case "has": return {
111
+ kind: "expr",
112
+ text: `${field}~${lit(value)}`
113
+ };
114
+ case "hasAny": return chain(" || ", (entry) => `${field}~${lit(entry)}`, false);
115
+ case "hasAll": return chain(" && ", (entry) => `${field}~${lit(entry)}`, true);
116
+ default: throw unsupportedOperator(field, op);
117
+ }
91
118
  }
92
119
  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;
120
+ const node = translateFilter(filter);
121
+ if (node.kind === "expr") return node.text;
122
+ if (node.kind === "const") return node.value ? void 0 : MATCHES_NO_RECORD;
97
123
  }
98
124
  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" };
125
+ if (record === null || typeof record !== "object") throw {
126
+ kind: "protocol",
127
+ code: "invalid-record",
128
+ message: "PocketBase returned a non-object record."
129
+ };
130
+ const id = record[idField];
131
+ if (typeof id !== "string" && (typeof id !== "number" || !Number.isFinite(id))) throw {
132
+ kind: "protocol",
133
+ code: "invalid-record-id",
134
+ message: `PocketBase record is missing a valid ${idField} field.`
135
+ };
136
+ return {
137
+ kind: "record",
138
+ id,
139
+ record,
140
+ completeness: "full"
141
+ };
115
142
  }
116
143
  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
- });
144
+ const base = opts.baseUrl.replace(/\/+$/, "");
145
+ const records = (resource) => `${base}/api/collections/${resource}/records`;
146
+ async function call(url, init = {}, signal) {
147
+ try {
148
+ const token = opts.getToken();
149
+ const headers = new Headers(init.headers);
150
+ if (token) headers.set("authorization", token);
151
+ if (init.body && !(init.body instanceof FormData)) headers.set("content-type", "application/json");
152
+ const response = await (opts.fetchImpl ?? fetch)(url, {
153
+ ...init,
154
+ headers,
155
+ signal
156
+ });
157
+ const text = await response.text();
158
+ let json = {};
159
+ if (text) try {
160
+ json = JSON.parse(text);
161
+ } catch {
162
+ throw {
163
+ kind: "protocol",
164
+ code: "invalid-json",
165
+ message: "PocketBase returned invalid JSON.",
166
+ status: response.status
167
+ };
168
+ }
169
+ if (!response.ok) throw {
170
+ kind: "transport",
171
+ code: "pocketbase-request-failed",
172
+ message: typeof json.message === "string" ? json.message : `PocketBase error ${response.status}`,
173
+ status: response.status,
174
+ details: json
175
+ };
176
+ return json;
177
+ } catch (error) {
178
+ if (error !== null && typeof error === "object" && "kind" in error) throw error;
179
+ throw {
180
+ kind: signal?.aborted ? "cancelled" : "transport",
181
+ code: signal?.aborted ? "request-aborted" : "pocketbase-request-failed",
182
+ message: error instanceof Error ? error.message : "PocketBase request failed.",
183
+ cause: error
184
+ };
185
+ }
186
+ }
187
+ const body = (input) => input instanceof FormData ? input : JSON.stringify(input);
188
+ return (0, _burojs_core.defineDataProvider)({
189
+ queries: {
190
+ async list(descriptor, context) {
191
+ const params = new URLSearchParams();
192
+ if (descriptor.pagination?.kind === "offset") {
193
+ params.set("page", String(descriptor.pagination.page));
194
+ params.set("perPage", String(descriptor.pagination.pageSize));
195
+ } else if (descriptor.pagination?.kind === "cursor") throw {
196
+ kind: "capability",
197
+ code: "cursor-pagination-unsupported",
198
+ message: "PocketBase provider supports offset pagination only."
199
+ };
200
+ const filter = filterExpression(descriptor.filter);
201
+ if (filter) params.set("filter", filter);
202
+ if (descriptor.sort?.length) params.set("sort", descriptor.sort.map(({ field, direction }) => `${direction === "desc" ? "-" : "+"}${field}`).join(","));
203
+ const json = await call(`${records(descriptor.resource)}?${params}`, {}, context.signal);
204
+ return {
205
+ data: json.items ?? [],
206
+ ...typeof json.totalItems === "number" ? { total: json.totalItems } : {}
207
+ };
208
+ },
209
+ async one(descriptor, context) {
210
+ return { data: await call(`${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`, {}, context.signal) };
211
+ },
212
+ async many(descriptor, context) {
213
+ if (descriptor.ids.length === 0) return { data: [] };
214
+ const filter = descriptor.ids.map((id) => `id=${lit(id)}`).join(" || ");
215
+ return { data: (await call(`${records(descriptor.resource)}?filter=${encodeURIComponent(filter)}&perPage=${descriptor.ids.length}`, {}, context.signal)).items ?? [] };
216
+ }
217
+ },
218
+ mutations: {
219
+ async createOne(descriptor, context) {
220
+ return recordResult(await call(records(descriptor.resource), {
221
+ method: "POST",
222
+ body: body(descriptor.input)
223
+ }, context.signal), descriptor.idField);
224
+ },
225
+ async updateOne(descriptor, context) {
226
+ return recordResult(await call(`${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`, {
227
+ method: "PATCH",
228
+ body: body(descriptor.input)
229
+ }, context.signal), descriptor.idField);
230
+ },
231
+ async deleteOne(descriptor, context) {
232
+ await call(`${records(descriptor.resource)}/${encodeURIComponent(String(descriptor.id))}`, { method: "DELETE" }, context.signal);
233
+ return {
234
+ kind: "identity",
235
+ id: descriptor.id
236
+ };
237
+ }
238
+ }
239
+ });
241
240
  }
242
-
243
- // src/auth-provider.ts
241
+ //#endregion
242
+ //#region src/auth-provider.ts
244
243
  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
- };
244
+ const base = opts.baseUrl.replace(/\/+$/, "");
245
+ const key = opts.storageKey ?? "buro-example-vite-auth";
246
+ const read = () => {
247
+ try {
248
+ const raw = localStorage.getItem(key);
249
+ if (!raw) return null;
250
+ const parsed = JSON.parse(raw);
251
+ if (!parsed || typeof parsed.token !== "string" || typeof parsed.record?.id !== "string") return null;
252
+ return parsed;
253
+ } catch {
254
+ return null;
255
+ }
256
+ };
257
+ return {
258
+ getToken: () => read()?.token ?? null,
259
+ async login(params) {
260
+ const { email, password } = params;
261
+ const res = await (opts.fetchImpl ?? fetch)(`${base}/api/collections/users/auth-with-password`, {
262
+ method: "POST",
263
+ headers: { "content-type": "application/json" },
264
+ body: JSON.stringify({
265
+ identity: email,
266
+ password
267
+ })
268
+ });
269
+ if (!res.ok) return {
270
+ success: false,
271
+ error: {
272
+ kind: "auth",
273
+ code: "invalid-credentials",
274
+ message: "Invalid credentials"
275
+ }
276
+ };
277
+ localStorage.setItem(key, JSON.stringify(await res.json()));
278
+ return { success: true };
279
+ },
280
+ async logout() {
281
+ localStorage.removeItem(key);
282
+ return { success: true };
283
+ },
284
+ async check() {
285
+ return read() ? { authenticated: true } : {
286
+ authenticated: false,
287
+ reason: "signed-out"
288
+ };
289
+ },
290
+ async onError(error) {
291
+ return {
292
+ action: "propagate",
293
+ error
294
+ };
295
+ },
296
+ async getIdentity() {
297
+ const session = read();
298
+ if (!session) return null;
299
+ return {
300
+ id: session.record.id,
301
+ name: session.record.name ?? session.record.email
302
+ };
303
+ }
304
+ };
295
305
  }
296
-
306
+ //#endregion
297
307
  exports.FILTER_OPERATOR_UNSUPPORTED_CODE = FILTER_OPERATOR_UNSUPPORTED_CODE;
298
308
  exports.createPocketbaseAuthProvider = createPocketbaseAuthProvider;
299
309
  exports.createPocketbaseDataProvider = createPocketbaseDataProvider;
300
310
  exports.filterExpression = filterExpression;
301
- //# sourceMappingURL=index.cjs.map
311
+
302
312
  //# sourceMappingURL=index.cjs.map