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