@anysoftinc/anydb-sdk 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -167
- package/dist/client.d.ts +45 -96
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +273 -305
- package/dist/datascript-backend.d.ts +26 -0
- package/dist/datascript-backend.d.ts.map +1 -0
- package/dist/datascript-backend.js +113 -0
- package/dist/nextauth-adapter.d.ts +7 -2
- package/dist/nextauth-adapter.d.ts.map +1 -1
- package/dist/nextauth-adapter.js +254 -149
- package/package.json +5 -2
- package/dist/query-builder.d.ts +0 -126
- package/dist/query-builder.d.ts.map +0 -1
- package/dist/query-builder.js +0 -207
package/dist/client.js
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
|
-
//
|
|
1
|
+
// AnyDB TypeScript SDK v2 - strict, no-fallback client
|
|
2
|
+
// - Explicit EDN symbolic types (Keyword, Symbol, UUID)
|
|
3
|
+
// - Strict SymbolicQuery API
|
|
4
|
+
// - Explicit DataScript selection via storage alias (no fallback)
|
|
5
|
+
// - Keywordized JS<->EDN conversion for object keys
|
|
2
6
|
import { parseEDNString as parseEdn } from "edn-data";
|
|
3
|
-
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { DataScriptBackend, shouldUseDataScript, isDataScriptAvailable, } from "./datascript-backend";
|
|
8
|
+
export const sym = (name) => ({ _type: "symbol", value: name });
|
|
9
|
+
export const uuid = (id) => ({ _type: "uuid", value: id });
|
|
10
|
+
export const kw = (name) => {
|
|
11
|
+
if (typeof name !== "string" || !name) {
|
|
12
|
+
throw new QueryInputError("kw() requires a non-empty string name");
|
|
13
|
+
}
|
|
14
|
+
if (name.startsWith(":")) {
|
|
15
|
+
throw new QueryInputError("kw() expects bare keyword without leading ':'", {
|
|
16
|
+
name,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
_type: "keyword",
|
|
21
|
+
value: name,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
// ===== Errors =====
|
|
25
|
+
class AnyDBError extends Error {
|
|
26
|
+
constructor(message, details) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.details = details;
|
|
29
|
+
this.name = this.constructor.name;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class AuthError extends AnyDBError {
|
|
33
|
+
}
|
|
34
|
+
export class QueryInputError extends AnyDBError {
|
|
35
|
+
}
|
|
36
|
+
export class SchemaError extends AnyDBError {
|
|
37
|
+
}
|
|
38
|
+
export class TransportError extends AnyDBError {
|
|
39
|
+
}
|
|
40
|
+
export class ServerError extends AnyDBError {
|
|
41
|
+
}
|
|
42
|
+
// ===== EDN Stringifier with keywordized key handling =====
|
|
9
43
|
export function stringifyEdn(obj) {
|
|
10
44
|
// Handle symbolic types first
|
|
11
45
|
if (typeof obj === "object" && obj !== null && "_type" in obj) {
|
|
12
46
|
const typed = obj;
|
|
13
47
|
switch (typed._type) {
|
|
14
|
-
case
|
|
48
|
+
case "symbol":
|
|
15
49
|
return typed.value; // ?e, ?id, etc.
|
|
16
|
-
case
|
|
50
|
+
case "uuid":
|
|
17
51
|
return `#uuid "${typed.value}"`;
|
|
18
|
-
case
|
|
19
|
-
|
|
52
|
+
case "keyword":
|
|
53
|
+
// Ensure leading colon when emitting
|
|
54
|
+
return `:${typed.value}`;
|
|
20
55
|
}
|
|
21
56
|
}
|
|
22
57
|
if (typeof obj === "string") {
|
|
23
|
-
//
|
|
24
|
-
if (obj.startsWith(":") || obj.startsWith("?")) {
|
|
25
|
-
return obj;
|
|
26
|
-
}
|
|
58
|
+
// Strings are always quoted; callers must use sym/kw for special values
|
|
27
59
|
return `"${obj.replace(/"/g, '\\"')}"`;
|
|
28
60
|
}
|
|
29
61
|
if (typeof obj === "number")
|
|
@@ -38,360 +70,296 @@ export function stringifyEdn(obj) {
|
|
|
38
70
|
return "[" + obj.map(stringifyEdn).join(" ") + "]";
|
|
39
71
|
}
|
|
40
72
|
if (typeof obj === "object") {
|
|
73
|
+
// Keywordize JS object keys by emitting them as EDN keywords
|
|
41
74
|
const pairs = Object.entries(obj).map(([key, value]) => {
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
if (key.startsWith(":")) {
|
|
76
|
+
throw new QueryInputError("Object keys representing keywords must not include a leading ':'", { key });
|
|
77
|
+
}
|
|
78
|
+
const ednKey = `:${key}`;
|
|
44
79
|
return `${ednKey} ${stringifyEdn(value)}`;
|
|
45
80
|
});
|
|
46
81
|
return "{" + pairs.join(" ") + "}";
|
|
47
82
|
}
|
|
48
83
|
return String(obj);
|
|
49
84
|
}
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
// Normalize edn-data parsed value into JS with keywordized keys (no leading colon)
|
|
86
|
+
function normalizeEdn(value) {
|
|
87
|
+
if (value === null || value === undefined)
|
|
88
|
+
return value;
|
|
89
|
+
// Preserve native Date instances returned by the parser
|
|
90
|
+
if (value instanceof Date)
|
|
91
|
+
return value;
|
|
92
|
+
// edn-data often wraps keywords/symbols/strings as { key: '...' } or { sym: '...' }
|
|
93
|
+
if (typeof value === "object" && value) {
|
|
94
|
+
if (typeof value.key === "string") {
|
|
95
|
+
const key = value.key; // e.g., ":db/ident"
|
|
96
|
+
return key.startsWith(":") ? key.slice(1) : key;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value.sym === "string") {
|
|
99
|
+
return value.sym;
|
|
63
100
|
}
|
|
64
|
-
// Sets -> Arrays
|
|
65
101
|
if (value instanceof Set) {
|
|
66
|
-
return Array.from(value, (v) =>
|
|
102
|
+
return Array.from(value, (v) => normalizeEdn(v));
|
|
67
103
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return value.set.map((v) => this.normalizeEdn(v));
|
|
104
|
+
if (Array.isArray(value.set)) {
|
|
105
|
+
return value.set.map((v) => normalizeEdn(v));
|
|
71
106
|
}
|
|
72
|
-
|
|
73
|
-
if (typeof value === 'object' && value && Array.isArray(value.map)) {
|
|
107
|
+
if (Array.isArray(value.map)) {
|
|
74
108
|
const out = {};
|
|
75
109
|
for (const [k, v] of value.map) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
110
|
+
const kNorm = normalizeEdn(k);
|
|
111
|
+
const kStr = typeof kNorm === "string" ? kNorm : String(kNorm);
|
|
112
|
+
const bareKey = kStr.startsWith(":") ? kStr.slice(1) : kStr;
|
|
113
|
+
out[bareKey] = normalizeEdn(v);
|
|
79
114
|
}
|
|
80
115
|
return out;
|
|
81
116
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return value.map((v) => normalizeEdn(v));
|
|
120
|
+
// Tagged values
|
|
121
|
+
if (typeof value === "object" &&
|
|
122
|
+
value &&
|
|
123
|
+
value.tag === "inst" &&
|
|
124
|
+
typeof value.val === "string") {
|
|
125
|
+
const d = new Date(value.val);
|
|
126
|
+
return isNaN(d.getTime()) ? value.val : d;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === "object" &&
|
|
129
|
+
value &&
|
|
130
|
+
value.tag === "uuid" &&
|
|
131
|
+
typeof value.val === "string") {
|
|
132
|
+
return { _type: "uuid", value: value.val };
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === "object") {
|
|
135
|
+
const out = Array.isArray(value) ? [] : {};
|
|
136
|
+
for (const [k, v] of Object.entries(value)) {
|
|
137
|
+
out[k] = normalizeEdn(v);
|
|
85
138
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
// ===== Validation =====
|
|
144
|
+
function isKeyword(v) {
|
|
145
|
+
return typeof v === "object" && v !== null && v._type === "keyword";
|
|
146
|
+
}
|
|
147
|
+
function isSymbol(v) {
|
|
148
|
+
return typeof v === "object" && v !== null && v._type === "symbol";
|
|
149
|
+
}
|
|
150
|
+
function validateSymbolicQuery(q) {
|
|
151
|
+
if (!q ||
|
|
152
|
+
typeof q !== "object" ||
|
|
153
|
+
!Array.isArray(q.find) ||
|
|
154
|
+
!Array.isArray(q.where)) {
|
|
155
|
+
throw new QueryInputError("Invalid query object: requires { find: [], where: [] }");
|
|
156
|
+
}
|
|
157
|
+
// Enforce attribute positions to be Keyword in where clauses when pattern triples
|
|
158
|
+
for (let i = 0; i < q.where.length; i++) {
|
|
159
|
+
const clause = q.where[i];
|
|
160
|
+
if (Array.isArray(clause) &&
|
|
161
|
+
clause.length >= 3 &&
|
|
162
|
+
!Array.isArray(clause[0])) {
|
|
163
|
+
const attr = clause[1];
|
|
164
|
+
if (!isKeyword(attr)) {
|
|
165
|
+
throw new QueryInputError("Attribute position in where clause must be a Keyword", {
|
|
166
|
+
clauseIndex: i,
|
|
167
|
+
clause,
|
|
168
|
+
});
|
|
103
169
|
}
|
|
104
|
-
return out;
|
|
105
170
|
}
|
|
106
|
-
return value;
|
|
107
171
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Accept: "application/edn",
|
|
119
|
-
...this.config.headers,
|
|
120
|
-
...options.headers,
|
|
121
|
-
},
|
|
122
|
-
...processedOptions,
|
|
123
|
-
});
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
throw new Error(`Datomic API error: ${response.status} ${response.statusText}`);
|
|
172
|
+
}
|
|
173
|
+
// ===== Root HTTP Client =====
|
|
174
|
+
export class DatomicClient {
|
|
175
|
+
constructor(config) {
|
|
176
|
+
this.config = config;
|
|
177
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
178
|
+
this.fetchImpl = (config.fetchImpl ||
|
|
179
|
+
globalThis.fetch);
|
|
180
|
+
if (!this.fetchImpl) {
|
|
181
|
+
throw new TransportError("fetch implementation is required in this environment");
|
|
126
182
|
}
|
|
127
|
-
// Parse EDN response
|
|
128
|
-
const responseText = await response.text();
|
|
129
|
-
const parsed = parseEdn(responseText);
|
|
130
|
-
return this.normalizeEdn(parsed);
|
|
131
183
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
async authHeader() {
|
|
185
|
+
let token = this.config.authToken;
|
|
186
|
+
if (!token && this.config.getAuthToken) {
|
|
187
|
+
const s = this.config.getAuthToken();
|
|
188
|
+
token = typeof s === "string" ? s : await s;
|
|
189
|
+
}
|
|
190
|
+
if (!token) {
|
|
191
|
+
throw new AuthError("Missing signing secret (provide authentication jwt or getAuthToken in client config)");
|
|
192
|
+
}
|
|
193
|
+
return { Authorization: `Bearer ${token}` };
|
|
135
194
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
async request(endpoint, body) {
|
|
196
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
197
|
+
const headers = {
|
|
198
|
+
"Content-Type": "application/edn",
|
|
199
|
+
Accept: "application/edn",
|
|
200
|
+
...(this.config.headers || {}),
|
|
201
|
+
...(await this.authHeader()),
|
|
202
|
+
};
|
|
203
|
+
const res = await this.fetchImpl(url, {
|
|
139
204
|
method: "POST",
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
// Delete database
|
|
144
|
-
async deleteDatabase(storageAlias, dbName) {
|
|
145
|
-
return this.request(`/data/${storageAlias}/${dbName}/`, {
|
|
146
|
-
method: "DELETE",
|
|
205
|
+
headers,
|
|
206
|
+
body: body === undefined ? undefined : stringifyEdn(body),
|
|
147
207
|
});
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
let serverBody = undefined;
|
|
210
|
+
try {
|
|
211
|
+
const txt = await res.text();
|
|
212
|
+
serverBody = normalizeEdn(parseEdn(txt));
|
|
213
|
+
}
|
|
214
|
+
catch (_) {
|
|
215
|
+
// ignore parse errors
|
|
216
|
+
}
|
|
217
|
+
throw new ServerError(`HTTP ${res.status} ${res.statusText}`, {
|
|
218
|
+
status: res.status,
|
|
219
|
+
endpoint,
|
|
220
|
+
serverBody,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const text = await res.text();
|
|
224
|
+
const parsed = parseEdn(text);
|
|
225
|
+
return normalizeEdn(parsed);
|
|
148
226
|
}
|
|
149
|
-
//
|
|
227
|
+
// Low-level endpoints (path-style)
|
|
150
228
|
async transact(storageAlias, dbName, txData) {
|
|
229
|
+
// Basic schema validation: txData must be an array of entity maps or operation vectors
|
|
230
|
+
if (!Array.isArray(txData)) {
|
|
231
|
+
throw new SchemaError("txData must be an array of entities/ops");
|
|
232
|
+
}
|
|
151
233
|
return this.request(`/data/${storageAlias}/${dbName}/`, {
|
|
152
|
-
|
|
153
|
-
body: { ":tx-data": txData },
|
|
234
|
+
"tx-data": txData,
|
|
154
235
|
});
|
|
155
236
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/`);
|
|
159
|
-
}
|
|
160
|
-
// Datoms operations
|
|
161
|
-
async datoms(storageAlias, dbName, basisT, options) {
|
|
162
|
-
const params = new URLSearchParams();
|
|
163
|
-
// Required parameter
|
|
164
|
-
params.append("index", options.index);
|
|
165
|
-
// Optional parameters
|
|
166
|
-
if (options.e !== undefined)
|
|
167
|
-
params.append("e", String(options.e));
|
|
168
|
-
if (options.a !== undefined)
|
|
169
|
-
params.append("a", String(options.a));
|
|
170
|
-
if (options.v !== undefined)
|
|
171
|
-
params.append("v", String(options.v));
|
|
172
|
-
if (options.start !== undefined)
|
|
173
|
-
params.append("start", String(options.start));
|
|
174
|
-
if (options.end !== undefined)
|
|
175
|
-
params.append("end", String(options.end));
|
|
176
|
-
if (options.limit !== undefined)
|
|
177
|
-
params.append("limit", String(options.limit));
|
|
178
|
-
if (options.offset !== undefined)
|
|
179
|
-
params.append("offset", String(options.offset));
|
|
180
|
-
if (options["as-of"] !== undefined)
|
|
181
|
-
params.append("as-of", String(options["as-of"]));
|
|
182
|
-
if (options.since !== undefined)
|
|
183
|
-
params.append("since", String(options.since));
|
|
184
|
-
if (options.history !== undefined)
|
|
185
|
-
params.append("history", String(options.history));
|
|
186
|
-
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/datoms?${params.toString()}`);
|
|
187
|
-
}
|
|
188
|
-
// Entity operations
|
|
189
|
-
async entity(storageAlias, dbName, basisT, entityId, options) {
|
|
190
|
-
const params = new URLSearchParams();
|
|
191
|
-
params.append("e", String(entityId));
|
|
192
|
-
if (options?.["as-of"] !== undefined)
|
|
193
|
-
params.append("as-of", String(options["as-of"]));
|
|
194
|
-
if (options?.since !== undefined)
|
|
195
|
-
params.append("since", String(options.since));
|
|
196
|
-
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/entity?${params.toString()}`);
|
|
197
|
-
}
|
|
198
|
-
// Query operations (GET version for simple queries)
|
|
199
|
-
async queryGet(query, args, options) {
|
|
200
|
-
const params = new URLSearchParams();
|
|
201
|
-
params.append("q", stringifyEdn(query));
|
|
202
|
-
params.append("args", stringifyEdn(args));
|
|
203
|
-
if (options?.limit !== undefined)
|
|
204
|
-
params.append("limit", String(options.limit));
|
|
205
|
-
if (options?.offset !== undefined)
|
|
206
|
-
params.append("offset", String(options.offset));
|
|
207
|
-
return this.request(`/api/query?${params.toString()}`);
|
|
208
|
-
}
|
|
209
|
-
// Query operations (POST version for complex queries)
|
|
210
|
-
async query(query, args, options) {
|
|
237
|
+
async querySymbolic(q, args = [], options) {
|
|
238
|
+
validateSymbolicQuery(q);
|
|
211
239
|
const body = {
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
q: [
|
|
241
|
+
kw("find"),
|
|
242
|
+
...q.find,
|
|
243
|
+
...(q.in && q.in.length ? [kw("in"), ...q.in] : []),
|
|
244
|
+
kw("where"),
|
|
245
|
+
...q.where,
|
|
246
|
+
],
|
|
247
|
+
args,
|
|
214
248
|
};
|
|
215
249
|
if (options?.limit !== undefined)
|
|
216
|
-
body["
|
|
250
|
+
body["limit"] = options.limit;
|
|
217
251
|
if (options?.offset !== undefined)
|
|
218
|
-
body["
|
|
219
|
-
|
|
220
|
-
method: "POST",
|
|
221
|
-
body: body,
|
|
222
|
-
});
|
|
223
|
-
// Ensure result is array of rows, not Set
|
|
224
|
-
if (res instanceof Set) {
|
|
225
|
-
return Array.from(res);
|
|
226
|
-
}
|
|
227
|
-
return res;
|
|
252
|
+
body["offset"] = options.offset;
|
|
253
|
+
return this.request(`/api/query`, body);
|
|
228
254
|
}
|
|
229
|
-
// Symbolic query operations
|
|
230
|
-
async querySymbolic(query, args = [], options) {
|
|
231
|
-
// Convert symbolic query to EDN vector format
|
|
232
|
-
const ednQuery = [":find", ...query.find];
|
|
233
|
-
if (query.in && query.in.length > 0) {
|
|
234
|
-
ednQuery.push(":in", ...query.in);
|
|
235
|
-
}
|
|
236
|
-
ednQuery.push(":where", ...query.where);
|
|
237
|
-
return this.query(ednQuery, args, options);
|
|
238
|
-
}
|
|
239
|
-
// Server-Sent Events for transaction reports
|
|
240
255
|
subscribeToEvents(storageAlias, dbName, onEvent, onError) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
eventSource.onerror = onError;
|
|
245
|
-
return eventSource;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Utility functions for common Datomic operations
|
|
249
|
-
export class DatomicUtils {
|
|
250
|
-
static tempId(partition = "db.part/user") {
|
|
251
|
-
return `tempid:${partition}:${Math.random().toString(36).substr(2, 9)}`;
|
|
252
|
-
}
|
|
253
|
-
static keyword(namespace, name) {
|
|
254
|
-
return `:${namespace}/${name}`;
|
|
255
|
-
}
|
|
256
|
-
static createEntity(attributes, tempId) {
|
|
257
|
-
const id = tempId || this.tempId();
|
|
258
|
-
return Object.entries(attributes).map(([attr, value]) => ({
|
|
259
|
-
"db/id": id,
|
|
260
|
-
[attr]: value,
|
|
261
|
-
}));
|
|
262
|
-
}
|
|
263
|
-
static retractEntity(entityId) {
|
|
264
|
-
return [[":db/retractEntity", entityId]];
|
|
265
|
-
}
|
|
266
|
-
static retractAttribute(entityId, attribute, value) {
|
|
267
|
-
if (value !== undefined) {
|
|
268
|
-
return [[":db/retract", entityId, attribute, value]];
|
|
256
|
+
const factory = this.config.eventSourceFactory || globalThis.EventSource;
|
|
257
|
+
if (!factory) {
|
|
258
|
+
throw new TransportError("EventSource not available. Provide eventSourceFactory in client config for Node.");
|
|
269
259
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
export class SchemaBuilder {
|
|
277
|
-
static attribute(def) {
|
|
278
|
-
return {
|
|
279
|
-
"db/id": DatomicUtils.tempId(),
|
|
280
|
-
...def,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
static enum(ident, doc) {
|
|
284
|
-
const enumDef = {
|
|
285
|
-
"db/id": DatomicUtils.tempId(),
|
|
286
|
-
":db/ident": ident,
|
|
287
|
-
};
|
|
288
|
-
if (doc) {
|
|
289
|
-
enumDef[":db/doc"] = doc;
|
|
290
|
-
}
|
|
291
|
-
return enumDef;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// EDN template literal for natural Datalog queries
|
|
295
|
-
export function edn(strings, ...values) {
|
|
296
|
-
// Combine template strings and interpolated values
|
|
297
|
-
let ednString = '';
|
|
298
|
-
for (let i = 0; i < strings.length; i++) {
|
|
299
|
-
ednString += strings[i];
|
|
300
|
-
if (i < values.length) {
|
|
301
|
-
const value = values[i];
|
|
302
|
-
// Convert interpolated values to EDN strings
|
|
303
|
-
ednString += stringifyEdn(value);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Use the proper EDN parser
|
|
307
|
-
const parsed = parseEdn(ednString);
|
|
308
|
-
// Apply normalization similar to the client's normalizeEdn method
|
|
309
|
-
return normalizeEdnForQuery(parsed);
|
|
310
|
-
}
|
|
311
|
-
function normalizeEdnForQuery(value) {
|
|
312
|
-
if (value === null || value === undefined)
|
|
313
|
-
return value;
|
|
314
|
-
// Handle edn-data keyword representations: {key: "find"} -> ":find"
|
|
315
|
-
if (typeof value === 'object' && value && typeof value.key === 'string') {
|
|
316
|
-
const key = value.key;
|
|
317
|
-
return key.startsWith(':') ? key : `:${key}`;
|
|
318
|
-
}
|
|
319
|
-
// Handle edn-data symbol representations: {sym: "?e"} -> "?e"
|
|
320
|
-
if (typeof value === 'object' && value && typeof value.sym === 'string') {
|
|
321
|
-
return value.sym;
|
|
322
|
-
}
|
|
323
|
-
// Handle edn-data list representations: {list: [...]} -> [...]
|
|
324
|
-
if (typeof value === 'object' && value && Array.isArray(value.list)) {
|
|
325
|
-
return value.list.map((v) => normalizeEdnForQuery(v));
|
|
326
|
-
}
|
|
327
|
-
// Sets -> Arrays
|
|
328
|
-
if (value instanceof Set) {
|
|
329
|
-
return Array.from(value, (v) => normalizeEdnForQuery(v));
|
|
330
|
-
}
|
|
331
|
-
// edn-data set representation: { set: [...] }
|
|
332
|
-
if (typeof value === 'object' && value && Array.isArray(value.set)) {
|
|
333
|
-
return value.set.map((v) => normalizeEdnForQuery(v));
|
|
334
|
-
}
|
|
335
|
-
// Arrays
|
|
336
|
-
if (Array.isArray(value)) {
|
|
337
|
-
return value.map((v) => normalizeEdnForQuery(v));
|
|
338
|
-
}
|
|
339
|
-
// Convert common UUID representations
|
|
340
|
-
if (typeof value === 'object' && value && value.tag === 'uuid' && typeof value.val === 'string') {
|
|
341
|
-
return { _type: 'uuid', value: value.val };
|
|
260
|
+
const url = `${this.baseUrl}/events/${storageAlias}/${dbName}`;
|
|
261
|
+
const es = new factory(url);
|
|
262
|
+
es.onmessage = onEvent;
|
|
263
|
+
if (onError)
|
|
264
|
+
es.onerror = onError;
|
|
265
|
+
return es;
|
|
342
266
|
}
|
|
343
|
-
return value;
|
|
344
267
|
}
|
|
345
|
-
// Convenience
|
|
346
|
-
export class
|
|
268
|
+
// ===== Convenience wrapper for a specific database =====
|
|
269
|
+
export class AnyDBClient {
|
|
347
270
|
constructor(client, storageAlias, dbName) {
|
|
348
271
|
this.client = client;
|
|
349
272
|
this.storageAlias = storageAlias;
|
|
350
273
|
this.dbName = dbName;
|
|
351
274
|
}
|
|
275
|
+
async info() {
|
|
276
|
+
if (shouldUseDataScript(this.storageAlias)) {
|
|
277
|
+
const backend = new DataScriptBackend(this.storageAlias, this.dbName);
|
|
278
|
+
return backend.databaseInfo();
|
|
279
|
+
}
|
|
280
|
+
// Exercise connectivity/auth via a trivial query; propagate errors to caller.
|
|
281
|
+
const q = { find: [sym("?e")], where: [[sym("?e"), kw("db/id"), 0]] };
|
|
282
|
+
await this.query(q);
|
|
283
|
+
return { "basis-t": -1, "db/alias": `${this.storageAlias}/${this.dbName}` };
|
|
284
|
+
}
|
|
352
285
|
async transact(txData) {
|
|
286
|
+
if (shouldUseDataScript(this.storageAlias)) {
|
|
287
|
+
const backend = new DataScriptBackend(this.storageAlias, this.dbName);
|
|
288
|
+
return backend.transact(txData);
|
|
289
|
+
}
|
|
353
290
|
return this.client.transact(this.storageAlias, this.dbName, txData);
|
|
354
291
|
}
|
|
355
|
-
async
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
292
|
+
async query(q, ...args) {
|
|
293
|
+
if (shouldUseDataScript(this.storageAlias)) {
|
|
294
|
+
const backend = new DataScriptBackend(this.storageAlias, this.dbName);
|
|
295
|
+
validateSymbolicQuery(q);
|
|
296
|
+
const ednQuery = [
|
|
297
|
+
kw("find"),
|
|
298
|
+
...q.find,
|
|
299
|
+
...(q.in && q.in.length ? [kw("in"), ...q.in] : []),
|
|
300
|
+
kw("where"),
|
|
301
|
+
...q.where,
|
|
302
|
+
];
|
|
303
|
+
return backend.query(ednQuery, ...args);
|
|
304
|
+
}
|
|
305
|
+
// Server path: use /api/query with db descriptor as first arg
|
|
368
306
|
const dbDescriptor = {
|
|
369
307
|
"db/alias": `${this.storageAlias}/${this.dbName}`,
|
|
370
308
|
};
|
|
371
|
-
return this.client.
|
|
309
|
+
return this.client.querySymbolic(q, [dbDescriptor, ...args]);
|
|
372
310
|
}
|
|
373
|
-
async
|
|
374
|
-
|
|
375
|
-
|
|
311
|
+
async entity(entityId, basisT = "-", options) {
|
|
312
|
+
if (shouldUseDataScript(this.storageAlias)) {
|
|
313
|
+
const backend = new DataScriptBackend(this.storageAlias, this.dbName);
|
|
314
|
+
return backend.entity(entityId);
|
|
315
|
+
}
|
|
316
|
+
// Use the query API to retrieve entity via pull
|
|
317
|
+
const q = {
|
|
318
|
+
find: [[sym("pull"), sym("?e"), [kw("db/id")]]],
|
|
319
|
+
where: [[sym("?e"), kw("db/id"), entityId]],
|
|
376
320
|
};
|
|
377
|
-
|
|
321
|
+
const rows = await this.query(q);
|
|
322
|
+
const first = Array.isArray(rows) ? rows[0]?.[0] : null;
|
|
323
|
+
return first || { "db/id": entityId };
|
|
324
|
+
}
|
|
325
|
+
async datoms(index, options = {}) {
|
|
326
|
+
if (shouldUseDataScript(this.storageAlias)) {
|
|
327
|
+
const backend = new DataScriptBackend(this.storageAlias, this.dbName);
|
|
328
|
+
return backend.datoms(index, {
|
|
329
|
+
e: options.e,
|
|
330
|
+
a: options.a,
|
|
331
|
+
v: options.v,
|
|
332
|
+
limit: options.limit,
|
|
333
|
+
offset: options.offset,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Server exposes datoms via query; implement minimal AVET example
|
|
337
|
+
const clauses = [];
|
|
338
|
+
const e = options.e !== undefined ? options.e : sym("?e");
|
|
339
|
+
const a = options.a !== undefined ? kw(options.a) : sym("?a");
|
|
340
|
+
const v = options.v !== undefined ? options.v : sym("?v");
|
|
341
|
+
clauses.push([e, a, v]);
|
|
342
|
+
const q = { find: [e, a, v], where: clauses };
|
|
343
|
+
const rows = await this.query(q);
|
|
344
|
+
return rows.map((r) => ({ e: r[0], a: r[1], v: r[2], tx: 0, added: true }));
|
|
378
345
|
}
|
|
379
346
|
subscribeToEvents(onEvent, onError) {
|
|
380
347
|
return this.client.subscribeToEvents(this.storageAlias, this.dbName, onEvent, onError);
|
|
381
348
|
}
|
|
382
349
|
}
|
|
383
|
-
//
|
|
350
|
+
// ===== Factories =====
|
|
384
351
|
export function createDatomicClient(config) {
|
|
385
352
|
return new DatomicClient(config);
|
|
386
353
|
}
|
|
387
|
-
export function
|
|
388
|
-
return new
|
|
354
|
+
export function createAnyDBClient(client, storageAlias, dbName) {
|
|
355
|
+
return new AnyDBClient(client, storageAlias, dbName);
|
|
389
356
|
}
|
|
390
|
-
//
|
|
357
|
+
// Convenience
|
|
391
358
|
export function pluckFirstColumn(rows) {
|
|
392
359
|
if (!Array.isArray(rows))
|
|
393
360
|
return [];
|
|
394
361
|
return rows.map((r) => (Array.isArray(r) ? r[0] : r));
|
|
395
362
|
}
|
|
396
|
-
//
|
|
363
|
+
// Re-export DataScript helpers for consumers that need feature-detection
|
|
364
|
+
export { shouldUseDataScript, isDataScriptAvailable };
|
|
397
365
|
export default DatomicClient;
|