@anysoftinc/anydb-sdk 0.1.1
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 +336 -0
- package/dist/client.d.ts +167 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +397 -0
- package/dist/nextauth-adapter.d.ts +23 -0
- package/dist/nextauth-adapter.d.ts.map +1 -0
- package/dist/nextauth-adapter.js +340 -0
- package/dist/query-builder.d.ts +126 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +207 -0
- package/package.json +65 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { SchemaBuilder, DatomicUtils, kw, sym, uuid as uuidEdn } from "./client";
|
|
2
|
+
// Datomic schema idents
|
|
3
|
+
const USER = {
|
|
4
|
+
id: ":auth.user.v1/id", // uuid identity
|
|
5
|
+
name: ":auth.user.v1/name",
|
|
6
|
+
email: ":auth.user.v1/email",
|
|
7
|
+
emailVerified: ":auth.user.v1/email-verified",
|
|
8
|
+
image: ":auth.user.v1/image",
|
|
9
|
+
};
|
|
10
|
+
const ACCOUNT = {
|
|
11
|
+
id: ":auth.account.v1/id", // uuid identity
|
|
12
|
+
userId: ":auth.account.v1/user-id", // uuid ref by value
|
|
13
|
+
type: ":auth.account.v1/type",
|
|
14
|
+
provider: ":auth.account.v1/provider",
|
|
15
|
+
providerAccountId: ":auth.account.v1/provider-account-id",
|
|
16
|
+
refreshToken: ":auth.account.v1/refresh-token",
|
|
17
|
+
accessToken: ":auth.account.v1/access-token",
|
|
18
|
+
expiresAt: ":auth.account.v1/expires-at",
|
|
19
|
+
tokenType: ":auth.account.v1/token-type",
|
|
20
|
+
scope: ":auth.account.v1/scope",
|
|
21
|
+
idToken: ":auth.account.v1/id-token",
|
|
22
|
+
sessionState: ":auth.account.v1/session-state",
|
|
23
|
+
};
|
|
24
|
+
const SESSION = {
|
|
25
|
+
id: ":auth.session.v1/id", // uuid identity
|
|
26
|
+
sessionToken: ":auth.session.v1/session-token",
|
|
27
|
+
userId: ":auth.session.v1/user-id",
|
|
28
|
+
expires: ":auth.session.v1/expires",
|
|
29
|
+
};
|
|
30
|
+
const VTOKEN = {
|
|
31
|
+
id: ":auth.vtoken.v1/id", // uuid identity
|
|
32
|
+
identifier: ":auth.vtoken.v1/identifier",
|
|
33
|
+
token: ":auth.vtoken.v1/token",
|
|
34
|
+
expires: ":auth.vtoken.v1/expires",
|
|
35
|
+
};
|
|
36
|
+
async function ensureAuthSchema(db) {
|
|
37
|
+
const defs = [];
|
|
38
|
+
// Helper to conditionally add attribute definitions
|
|
39
|
+
const addAttr = async (ident, vt, card, opts = {}) => {
|
|
40
|
+
const exists = await db.querySymbolic({ find: [sym("?e")], where: [[sym("?e"), kw(":db/ident"), kw(ident)]] });
|
|
41
|
+
if (!exists || (Array.isArray(exists) && exists.length === 0)) {
|
|
42
|
+
defs.push(SchemaBuilder.attribute({ ":db/ident": ident, ":db/valueType": vt, ":db/cardinality": card, ...opts }));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// User attributes
|
|
46
|
+
await addAttr(USER.id, ":db.type/uuid", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
47
|
+
await addAttr(USER.name, ":db.type/string", ":db.cardinality/one");
|
|
48
|
+
// Make email an identity attribute so we can upsert users by email
|
|
49
|
+
await addAttr(USER.email, ":db.type/string", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
50
|
+
await addAttr(USER.emailVerified, ":db.type/instant", ":db.cardinality/one");
|
|
51
|
+
await addAttr(USER.image, ":db.type/string", ":db.cardinality/one");
|
|
52
|
+
// Account attributes
|
|
53
|
+
await addAttr(ACCOUNT.id, ":db.type/uuid", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
54
|
+
await addAttr(ACCOUNT.userId, ":db.type/uuid", ":db.cardinality/one", { ":db/index": true });
|
|
55
|
+
await addAttr(ACCOUNT.type, ":db.type/string", ":db.cardinality/one");
|
|
56
|
+
await addAttr(ACCOUNT.provider, ":db.type/string", ":db.cardinality/one", { ":db/index": true });
|
|
57
|
+
await addAttr(ACCOUNT.providerAccountId, ":db.type/string", ":db.cardinality/one", { ":db/index": true });
|
|
58
|
+
await addAttr(ACCOUNT.refreshToken, ":db.type/string", ":db.cardinality/one");
|
|
59
|
+
await addAttr(ACCOUNT.accessToken, ":db.type/string", ":db.cardinality/one");
|
|
60
|
+
await addAttr(ACCOUNT.expiresAt, ":db.type/long", ":db.cardinality/one");
|
|
61
|
+
await addAttr(ACCOUNT.tokenType, ":db.type/string", ":db.cardinality/one");
|
|
62
|
+
await addAttr(ACCOUNT.scope, ":db.type/string", ":db.cardinality/one");
|
|
63
|
+
await addAttr(ACCOUNT.idToken, ":db.type/string", ":db.cardinality/one");
|
|
64
|
+
await addAttr(ACCOUNT.sessionState, ":db.type/string", ":db.cardinality/one");
|
|
65
|
+
// Session attributes
|
|
66
|
+
await addAttr(SESSION.id, ":db.type/uuid", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
67
|
+
await addAttr(SESSION.userId, ":db.type/uuid", ":db.cardinality/one", { ":db/index": true });
|
|
68
|
+
await addAttr(SESSION.sessionToken, ":db.type/string", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
69
|
+
await addAttr(SESSION.expires, ":db.type/instant", ":db.cardinality/one");
|
|
70
|
+
// Verification token attributes
|
|
71
|
+
await addAttr(VTOKEN.id, ":db.type/uuid", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
72
|
+
await addAttr(VTOKEN.identifier, ":db.type/string", ":db.cardinality/one", { ":db/index": true });
|
|
73
|
+
await addAttr(VTOKEN.token, ":db.type/string", ":db.cardinality/one", { ":db/unique": ":db.unique/identity", ":db/index": true });
|
|
74
|
+
await addAttr(VTOKEN.expires, ":db.type/instant", ":db.cardinality/one");
|
|
75
|
+
// Install schema if there are new definitions
|
|
76
|
+
if (defs.length) {
|
|
77
|
+
try {
|
|
78
|
+
await db.transact(defs);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// Schema attributes may already exist, which is fine
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function toUser(m) {
|
|
86
|
+
const get = (k) => m && (m[k] ?? m[`:${k}`]);
|
|
87
|
+
// USER.id is ":auth.user.v1/id", so slice(1) gives "auth.user.v1/id"
|
|
88
|
+
const idKey = USER.id.slice(1); // "auth.user.v1/id"
|
|
89
|
+
const idVal = get(idKey);
|
|
90
|
+
const id = typeof idVal === 'string' ? idVal : idVal?.value ?? String(idVal);
|
|
91
|
+
const emailVerified = get(USER.emailVerified.slice(1));
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
name: get(USER.name.slice(1)) || null,
|
|
95
|
+
email: get(USER.email.slice(1)) || null,
|
|
96
|
+
emailVerified: emailVerified ? new Date(emailVerified) : null,
|
|
97
|
+
image: get(USER.image.slice(1)) || null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates a NextAuth.js adapter for AnyDB/Datomic
|
|
102
|
+
*
|
|
103
|
+
* @param db - DatomicDatabase instance
|
|
104
|
+
* @returns NextAuth.js Adapter
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { createDatomicDatabase } from '@anysoftinc/anydb-sdk';
|
|
109
|
+
* import { AnyDBAdapter } from '@anysoftinc/anydb-sdk/nextauth-adapter';
|
|
110
|
+
*
|
|
111
|
+
* const db = createDatomicDatabase(client, 'storage', 'auth-db');
|
|
112
|
+
*
|
|
113
|
+
* export default NextAuth({
|
|
114
|
+
* adapter: AnyDBAdapter(db),
|
|
115
|
+
* // ... other config
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function AnyDBAdapter(db) {
|
|
120
|
+
async function getUserByEmail(email) {
|
|
121
|
+
await ensureAuthSchema(db);
|
|
122
|
+
const q = `[:find (pull ?e [${USER.id} ${USER.name} ${USER.email} ${USER.emailVerified} ${USER.image}]) :where [?e ${USER.email} "${email}"]]`;
|
|
123
|
+
const res = await db.query(q);
|
|
124
|
+
const rows = Array.isArray(res) ? res : [];
|
|
125
|
+
return rows.length ? toUser(rows[0][0]) : null;
|
|
126
|
+
}
|
|
127
|
+
async function getUser(id) {
|
|
128
|
+
await ensureAuthSchema(db);
|
|
129
|
+
const q = `[:find (pull ?e [${USER.id} ${USER.name} ${USER.email} ${USER.emailVerified} ${USER.image}]) :where [?e ${USER.id} #uuid "${id}"]]`;
|
|
130
|
+
const res = await db.query(q);
|
|
131
|
+
const rows = Array.isArray(res) ? res : [];
|
|
132
|
+
if (!rows.length)
|
|
133
|
+
return null;
|
|
134
|
+
return toUser(rows[0][0]);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
// Users
|
|
138
|
+
async createUser(data) {
|
|
139
|
+
await ensureAuthSchema(db);
|
|
140
|
+
// Check if user already exists by email
|
|
141
|
+
if (data.email) {
|
|
142
|
+
const existing = await getUserByEmail(data.email);
|
|
143
|
+
if (existing) {
|
|
144
|
+
return existing;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const id = globalThis.crypto?.randomUUID?.() || (await import("crypto")).randomUUID();
|
|
148
|
+
const tx = {
|
|
149
|
+
'db/id': DatomicUtils.tempId(),
|
|
150
|
+
[USER.id]: uuidEdn(id),
|
|
151
|
+
...(data.name ? { [USER.name]: data.name } : {}),
|
|
152
|
+
...(data.email ? { [USER.email]: data.email } : {}),
|
|
153
|
+
...(data.emailVerified ? { [USER.emailVerified]: data.emailVerified } : {}),
|
|
154
|
+
...(data.image ? { [USER.image]: data.image } : {}),
|
|
155
|
+
};
|
|
156
|
+
await db.transact([tx]);
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
name: data.name ?? null,
|
|
160
|
+
email: data.email ?? null,
|
|
161
|
+
emailVerified: data.emailVerified ?? null,
|
|
162
|
+
image: data.image ?? null
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
getUser,
|
|
166
|
+
getUserByEmail,
|
|
167
|
+
async getUserByAccount({ provider, providerAccountId }) {
|
|
168
|
+
await ensureAuthSchema(db);
|
|
169
|
+
const q = `[:find (pull ?u [${USER.id} ${USER.name} ${USER.email} ${USER.emailVerified} ${USER.image}])
|
|
170
|
+
:where [?a ${ACCOUNT.provider} "${provider}"]
|
|
171
|
+
[?a ${ACCOUNT.providerAccountId} "${providerAccountId}"]
|
|
172
|
+
[?a ${ACCOUNT.userId} ?uid]
|
|
173
|
+
[?u ${USER.id} ?uid]]`;
|
|
174
|
+
const res = await db.query(q);
|
|
175
|
+
const rows = Array.isArray(res) ? res : [];
|
|
176
|
+
return rows.length ? toUser(rows[0][0]) : null;
|
|
177
|
+
},
|
|
178
|
+
async updateUser(data) {
|
|
179
|
+
await ensureAuthSchema(db);
|
|
180
|
+
if (!data.id)
|
|
181
|
+
throw new Error("updateUser requires id");
|
|
182
|
+
const tx = {
|
|
183
|
+
'db/id': DatomicUtils.tempId(),
|
|
184
|
+
[USER.id]: uuidEdn(data.id),
|
|
185
|
+
...(data.name !== undefined ? { [USER.name]: data.name } : {}),
|
|
186
|
+
...(data.email !== undefined ? { [USER.email]: data.email } : {}),
|
|
187
|
+
...(data.emailVerified !== undefined ? { [USER.emailVerified]: data.emailVerified } : {}),
|
|
188
|
+
...(data.image !== undefined ? { [USER.image]: data.image } : {}),
|
|
189
|
+
};
|
|
190
|
+
await db.transact([tx]);
|
|
191
|
+
const u = await getUser(data.id);
|
|
192
|
+
return u;
|
|
193
|
+
},
|
|
194
|
+
async deleteUser(id) {
|
|
195
|
+
await ensureAuthSchema(db);
|
|
196
|
+
await db.transact([[":db/retractEntity", [USER.id], uuidEdn(id)]]);
|
|
197
|
+
return null;
|
|
198
|
+
},
|
|
199
|
+
// Accounts
|
|
200
|
+
async linkAccount(account) {
|
|
201
|
+
await ensureAuthSchema(db);
|
|
202
|
+
const id = globalThis.crypto?.randomUUID?.() || (await import("crypto")).randomUUID();
|
|
203
|
+
const tx = {
|
|
204
|
+
'db/id': DatomicUtils.tempId(),
|
|
205
|
+
[ACCOUNT.id]: uuidEdn(id),
|
|
206
|
+
[ACCOUNT.userId]: uuidEdn(account.userId),
|
|
207
|
+
[ACCOUNT.type]: account.type,
|
|
208
|
+
[ACCOUNT.provider]: account.provider,
|
|
209
|
+
[ACCOUNT.providerAccountId]: account.providerAccountId,
|
|
210
|
+
...(account.refresh_token ? { [ACCOUNT.refreshToken]: account.refresh_token } : {}),
|
|
211
|
+
...(account.access_token ? { [ACCOUNT.accessToken]: account.access_token } : {}),
|
|
212
|
+
...(account.expires_at ? { [ACCOUNT.expiresAt]: account.expires_at } : {}),
|
|
213
|
+
...(account.token_type ? { [ACCOUNT.tokenType]: account.token_type } : {}),
|
|
214
|
+
...(account.scope ? { [ACCOUNT.scope]: account.scope } : {}),
|
|
215
|
+
...(account.id_token ? { [ACCOUNT.idToken]: account.id_token } : {}),
|
|
216
|
+
...(account.session_state ? { [ACCOUNT.sessionState]: account.session_state } : {}),
|
|
217
|
+
};
|
|
218
|
+
await db.transact([tx]);
|
|
219
|
+
return account;
|
|
220
|
+
},
|
|
221
|
+
async unlinkAccount({ provider, providerAccountId }) {
|
|
222
|
+
await ensureAuthSchema(db);
|
|
223
|
+
const q = `[:find ?id :where [?a ${ACCOUNT.provider} "${provider}"] [?a ${ACCOUNT.providerAccountId} "${providerAccountId}"] [?a ${ACCOUNT.id} ?id]]`;
|
|
224
|
+
const res = await db.query(q);
|
|
225
|
+
const id = Array.isArray(res) && res[0]?.[0];
|
|
226
|
+
if (id) {
|
|
227
|
+
await db.transact([[":db/retractEntity", [ACCOUNT.id], id]]);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
// Sessions
|
|
231
|
+
async createSession(session) {
|
|
232
|
+
await ensureAuthSchema(db);
|
|
233
|
+
const id = globalThis.crypto?.randomUUID?.() || (await import("crypto")).randomUUID();
|
|
234
|
+
let expiresDate;
|
|
235
|
+
if (session.expires instanceof Date && !isNaN(session.expires.getTime())) {
|
|
236
|
+
expiresDate = session.expires;
|
|
237
|
+
}
|
|
238
|
+
else if (session.expires) {
|
|
239
|
+
const d = new Date(session.expires);
|
|
240
|
+
expiresDate = isNaN(d.getTime()) ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : d;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
expiresDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
244
|
+
}
|
|
245
|
+
const tx = {
|
|
246
|
+
'db/id': DatomicUtils.tempId(),
|
|
247
|
+
[SESSION.id]: uuidEdn(id),
|
|
248
|
+
[SESSION.userId]: uuidEdn(session.userId),
|
|
249
|
+
[SESSION.sessionToken]: session.sessionToken,
|
|
250
|
+
[SESSION.expires]: expiresDate,
|
|
251
|
+
};
|
|
252
|
+
await db.transact([tx]);
|
|
253
|
+
return { ...session, expires: expiresDate };
|
|
254
|
+
},
|
|
255
|
+
async getSessionAndUser(sessionToken) {
|
|
256
|
+
await ensureAuthSchema(db);
|
|
257
|
+
const q = `[:find (pull ?s [${SESSION.id} ${SESSION.sessionToken} ${SESSION.userId} ${SESSION.expires}]) (pull ?u [${USER.id} ${USER.name} ${USER.email} ${USER.emailVerified} ${USER.image}])
|
|
258
|
+
:where [?s ${SESSION.sessionToken} "${sessionToken}"]
|
|
259
|
+
[?s ${SESSION.userId} ?uid]
|
|
260
|
+
[?u ${USER.id} ?uid]]`;
|
|
261
|
+
const res = await db.query(q);
|
|
262
|
+
const row = Array.isArray(res) ? res[0] : null;
|
|
263
|
+
if (!row)
|
|
264
|
+
return null;
|
|
265
|
+
const s = row[0];
|
|
266
|
+
const u = row[1];
|
|
267
|
+
const getS = (k) => s && (s[k] ?? s[`:${k}`]);
|
|
268
|
+
const expiresRaw = getS(SESSION.expires.slice(1));
|
|
269
|
+
const expires = expiresRaw instanceof Date ? expiresRaw : new Date(expiresRaw);
|
|
270
|
+
const sessionResult = {
|
|
271
|
+
sessionToken: getS(SESSION.sessionToken.slice(1)),
|
|
272
|
+
userId: (getS(SESSION.userId.slice(1))?.value ?? getS(SESSION.userId.slice(1))),
|
|
273
|
+
expires,
|
|
274
|
+
};
|
|
275
|
+
return { session: sessionResult, user: toUser(u) };
|
|
276
|
+
},
|
|
277
|
+
async updateSession(partial) {
|
|
278
|
+
await ensureAuthSchema(db);
|
|
279
|
+
let expiresDate;
|
|
280
|
+
if (partial.expires instanceof Date) {
|
|
281
|
+
expiresDate = isNaN(partial.expires.getTime()) ? undefined : partial.expires;
|
|
282
|
+
}
|
|
283
|
+
else if (partial.expires) {
|
|
284
|
+
const d = new Date(partial.expires);
|
|
285
|
+
expiresDate = isNaN(d.getTime()) ? undefined : d;
|
|
286
|
+
}
|
|
287
|
+
const tx = {
|
|
288
|
+
'db/id': DatomicUtils.tempId(),
|
|
289
|
+
...(partial.sessionToken ? { [SESSION.sessionToken]: partial.sessionToken } : {}),
|
|
290
|
+
...(partial.userId ? { [SESSION.userId]: uuidEdn(partial.userId) } : {}),
|
|
291
|
+
...(expiresDate ? { [SESSION.expires]: expiresDate } : {}),
|
|
292
|
+
};
|
|
293
|
+
await db.transact([tx]);
|
|
294
|
+
return { ...partial, ...(expiresDate ? { expires: expiresDate } : {}) };
|
|
295
|
+
},
|
|
296
|
+
async deleteSession(sessionToken) {
|
|
297
|
+
await ensureAuthSchema(db);
|
|
298
|
+
const q = `[:find ?id :where [?s ${SESSION.sessionToken} "${sessionToken}"] [?s ${SESSION.id} ?id]]`;
|
|
299
|
+
const res = await db.query(q);
|
|
300
|
+
const id = Array.isArray(res) && res[0]?.[0];
|
|
301
|
+
if (id) {
|
|
302
|
+
await db.transact([[":db/retractEntity", [SESSION.id], id]]);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
// Verification tokens (email sign-in, passwordless)
|
|
306
|
+
async createVerificationToken(token) {
|
|
307
|
+
await ensureAuthSchema(db);
|
|
308
|
+
const id = globalThis.crypto?.randomUUID?.() || (await import("crypto")).randomUUID();
|
|
309
|
+
const tx = {
|
|
310
|
+
'db/id': DatomicUtils.tempId(),
|
|
311
|
+
[VTOKEN.id]: uuidEdn(id),
|
|
312
|
+
[VTOKEN.identifier]: token.identifier,
|
|
313
|
+
[VTOKEN.token]: token.token,
|
|
314
|
+
[VTOKEN.expires]: token.expires,
|
|
315
|
+
};
|
|
316
|
+
await db.transact([tx]);
|
|
317
|
+
return token;
|
|
318
|
+
},
|
|
319
|
+
async useVerificationToken(params) {
|
|
320
|
+
await ensureAuthSchema(db);
|
|
321
|
+
const q = `[:find (pull ?e [${VTOKEN.id} ${VTOKEN.identifier} ${VTOKEN.token} ${VTOKEN.expires}])
|
|
322
|
+
:where [?e ${VTOKEN.identifier} "${params.identifier}"]
|
|
323
|
+
[?e ${VTOKEN.token} "${params.token}"]]`;
|
|
324
|
+
const res = await db.query(q);
|
|
325
|
+
const rows = Array.isArray(res) ? res : [];
|
|
326
|
+
if (!rows.length)
|
|
327
|
+
return null;
|
|
328
|
+
const m = rows[0][0];
|
|
329
|
+
const get = (k) => m && (m[k] ?? m[`:${k}`]);
|
|
330
|
+
const entityId = get(VTOKEN.id.slice(1));
|
|
331
|
+
// Delete the token after using it
|
|
332
|
+
await db.transact([[":db/retractEntity", [VTOKEN.id], entityId]]);
|
|
333
|
+
return {
|
|
334
|
+
identifier: get(VTOKEN.identifier.slice(1)),
|
|
335
|
+
token: get(VTOKEN.token.slice(1)),
|
|
336
|
+
expires: new Date(get(VTOKEN.expires.slice(1))),
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { QueryInput, QueryResult, QueryValue, Keyword, Symbol } from './client';
|
|
2
|
+
import type { DatomicDatabase } from './client';
|
|
3
|
+
export type QueryVariable = Symbol;
|
|
4
|
+
export interface QueryPattern {
|
|
5
|
+
entity: QueryValue;
|
|
6
|
+
attribute: QueryValue;
|
|
7
|
+
value: QueryValue;
|
|
8
|
+
tx?: QueryValue;
|
|
9
|
+
}
|
|
10
|
+
export interface AggregationClause {
|
|
11
|
+
function: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count-distinct';
|
|
12
|
+
variable: QueryVariable;
|
|
13
|
+
alias?: QueryVariable;
|
|
14
|
+
}
|
|
15
|
+
export declare class QueryBuilder {
|
|
16
|
+
private findClause;
|
|
17
|
+
private whereClause;
|
|
18
|
+
private inClause;
|
|
19
|
+
private withClause;
|
|
20
|
+
/**
|
|
21
|
+
* Add variables or aggregations to the :find clause
|
|
22
|
+
* @example
|
|
23
|
+
* .find(sym('?e'), sym('?name')) // Find entity and name
|
|
24
|
+
* .find({ function: 'count', variable: sym('?e') }) // Count entities
|
|
25
|
+
*/
|
|
26
|
+
find(...vars: (QueryVariable | AggregationClause)[]): this;
|
|
27
|
+
/**
|
|
28
|
+
* Add a structured where clause
|
|
29
|
+
* @example
|
|
30
|
+
* .where({ entity: sym('?e'), attribute: kw(':person/name'), value: sym('?name') })
|
|
31
|
+
*/
|
|
32
|
+
where(clause: QueryPattern): this;
|
|
33
|
+
/**
|
|
34
|
+
* Helper method to add entity-attribute-value pattern
|
|
35
|
+
* @example
|
|
36
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).withValue(sym('?name'))
|
|
37
|
+
*/
|
|
38
|
+
entity(entity: QueryValue): EntityQueryBuilder;
|
|
39
|
+
/**
|
|
40
|
+
* Add input parameters to the :in clause
|
|
41
|
+
* @example
|
|
42
|
+
* .in(sym('$')) // Database input
|
|
43
|
+
* .in(sym('?name')) // Parameter input
|
|
44
|
+
*/
|
|
45
|
+
in(...inputs: QueryVariable[]): this;
|
|
46
|
+
/**
|
|
47
|
+
* Add variables to the :with clause
|
|
48
|
+
* @example
|
|
49
|
+
* .with(sym('?tx')) // Include transaction in results
|
|
50
|
+
*/
|
|
51
|
+
with(...variables: QueryVariable[]): this;
|
|
52
|
+
/**
|
|
53
|
+
* Helper for common patterns - find entities with attribute
|
|
54
|
+
* @example
|
|
55
|
+
* .findEntitiesWith(kw(':person/name')) // Find entities with any name
|
|
56
|
+
* .findEntitiesWith(kw(':person/name'), sym('?name')) // Bind name to variable
|
|
57
|
+
* .findEntitiesWith(kw(':person/name'), 'John') // Find entities named John
|
|
58
|
+
*/
|
|
59
|
+
findEntitiesWith(attribute: Keyword, value?: QueryValue): this;
|
|
60
|
+
/**
|
|
61
|
+
* Helper for aggregation queries
|
|
62
|
+
* @example
|
|
63
|
+
* .count(sym('?e')) // Count entities
|
|
64
|
+
* .sum(sym('?amount')) // Sum amounts
|
|
65
|
+
*/
|
|
66
|
+
count(variable: QueryVariable, alias?: QueryVariable): this;
|
|
67
|
+
sum(variable: QueryVariable, alias?: QueryVariable): this;
|
|
68
|
+
avg(variable: QueryVariable, alias?: QueryVariable): this;
|
|
69
|
+
min(variable: QueryVariable, alias?: QueryVariable): this;
|
|
70
|
+
max(variable: QueryVariable, alias?: QueryVariable): this;
|
|
71
|
+
countDistinct(variable: QueryVariable, alias?: QueryVariable): this;
|
|
72
|
+
/**
|
|
73
|
+
* Build the final query array with proper EDN types
|
|
74
|
+
*/
|
|
75
|
+
build(): QueryInput;
|
|
76
|
+
/**
|
|
77
|
+
* Execute the query directly (requires database context)
|
|
78
|
+
*/
|
|
79
|
+
execute(db: DatomicDatabase, ...args: any[]): Promise<QueryResult>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Helper class for fluent entity-based query building
|
|
83
|
+
*/
|
|
84
|
+
export declare class EntityQueryBuilder {
|
|
85
|
+
private queryBuilder;
|
|
86
|
+
private entity;
|
|
87
|
+
constructor(queryBuilder: QueryBuilder, entity: QueryValue);
|
|
88
|
+
/**
|
|
89
|
+
* Add an attribute to the current entity
|
|
90
|
+
* @example
|
|
91
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name'))
|
|
92
|
+
*/
|
|
93
|
+
hasAttribute(attribute: Keyword): AttributeQueryBuilder;
|
|
94
|
+
/**
|
|
95
|
+
* Return to the main query builder
|
|
96
|
+
*/
|
|
97
|
+
build(): QueryInput;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Helper class for fluent attribute-based query building
|
|
101
|
+
*/
|
|
102
|
+
export declare class AttributeQueryBuilder {
|
|
103
|
+
private queryBuilder;
|
|
104
|
+
private entity;
|
|
105
|
+
private attribute;
|
|
106
|
+
constructor(queryBuilder: QueryBuilder, entity: QueryValue, attribute: Keyword);
|
|
107
|
+
/**
|
|
108
|
+
* Add a value constraint to the attribute
|
|
109
|
+
* @example
|
|
110
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).withValue(sym('?name'))
|
|
111
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/age')).withValue(25)
|
|
112
|
+
*/
|
|
113
|
+
withValue(value: QueryValue): QueryBuilder;
|
|
114
|
+
/**
|
|
115
|
+
* Add the attribute without a specific value constraint (will bind to a variable)
|
|
116
|
+
* @example
|
|
117
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).bind()
|
|
118
|
+
*/
|
|
119
|
+
bind(variable?: QueryVariable): QueryBuilder;
|
|
120
|
+
private generateVariable;
|
|
121
|
+
/**
|
|
122
|
+
* Return to the main query builder
|
|
123
|
+
*/
|
|
124
|
+
build(): QueryInput;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=query-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../src/query-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { kw, sym } from './client';
|
|
2
|
+
export class QueryBuilder {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.findClause = [];
|
|
5
|
+
this.whereClause = [];
|
|
6
|
+
this.inClause = [];
|
|
7
|
+
this.withClause = [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Add variables or aggregations to the :find clause
|
|
11
|
+
* @example
|
|
12
|
+
* .find(sym('?e'), sym('?name')) // Find entity and name
|
|
13
|
+
* .find({ function: 'count', variable: sym('?e') }) // Count entities
|
|
14
|
+
*/
|
|
15
|
+
find(...vars) {
|
|
16
|
+
this.findClause.push(...vars);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Add a structured where clause
|
|
21
|
+
* @example
|
|
22
|
+
* .where({ entity: sym('?e'), attribute: kw(':person/name'), value: sym('?name') })
|
|
23
|
+
*/
|
|
24
|
+
where(clause) {
|
|
25
|
+
this.whereClause.push(clause);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Helper method to add entity-attribute-value pattern
|
|
30
|
+
* @example
|
|
31
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).withValue(sym('?name'))
|
|
32
|
+
*/
|
|
33
|
+
entity(entity) {
|
|
34
|
+
return new EntityQueryBuilder(this, entity);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Add input parameters to the :in clause
|
|
38
|
+
* @example
|
|
39
|
+
* .in(sym('$')) // Database input
|
|
40
|
+
* .in(sym('?name')) // Parameter input
|
|
41
|
+
*/
|
|
42
|
+
in(...inputs) {
|
|
43
|
+
this.inClause.push(...inputs);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Add variables to the :with clause
|
|
48
|
+
* @example
|
|
49
|
+
* .with(sym('?tx')) // Include transaction in results
|
|
50
|
+
*/
|
|
51
|
+
with(...variables) {
|
|
52
|
+
this.withClause.push(...variables);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Helper for common patterns - find entities with attribute
|
|
57
|
+
* @example
|
|
58
|
+
* .findEntitiesWith(kw(':person/name')) // Find entities with any name
|
|
59
|
+
* .findEntitiesWith(kw(':person/name'), sym('?name')) // Bind name to variable
|
|
60
|
+
* .findEntitiesWith(kw(':person/name'), 'John') // Find entities named John
|
|
61
|
+
*/
|
|
62
|
+
findEntitiesWith(attribute, value) {
|
|
63
|
+
const entity = sym('?e');
|
|
64
|
+
const valueToUse = value !== undefined ? value : sym('?value');
|
|
65
|
+
// Only add value to find clause if it's a variable
|
|
66
|
+
if (value === undefined || (typeof valueToUse === 'object' && valueToUse && valueToUse._type === 'symbol')) {
|
|
67
|
+
this.find(entity, valueToUse);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.find(entity);
|
|
71
|
+
}
|
|
72
|
+
this.where({ entity, attribute, value: valueToUse });
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Helper for aggregation queries
|
|
77
|
+
* @example
|
|
78
|
+
* .count(sym('?e')) // Count entities
|
|
79
|
+
* .sum(sym('?amount')) // Sum amounts
|
|
80
|
+
*/
|
|
81
|
+
count(variable, alias) {
|
|
82
|
+
return this.find({ function: 'count', variable, alias });
|
|
83
|
+
}
|
|
84
|
+
sum(variable, alias) {
|
|
85
|
+
return this.find({ function: 'sum', variable, alias });
|
|
86
|
+
}
|
|
87
|
+
avg(variable, alias) {
|
|
88
|
+
return this.find({ function: 'avg', variable, alias });
|
|
89
|
+
}
|
|
90
|
+
min(variable, alias) {
|
|
91
|
+
return this.find({ function: 'min', variable, alias });
|
|
92
|
+
}
|
|
93
|
+
max(variable, alias) {
|
|
94
|
+
return this.find({ function: 'max', variable, alias });
|
|
95
|
+
}
|
|
96
|
+
countDistinct(variable, alias) {
|
|
97
|
+
return this.find({ function: 'count-distinct', variable, alias });
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the final query array with proper EDN types
|
|
101
|
+
*/
|
|
102
|
+
build() {
|
|
103
|
+
const query = [kw(":find")];
|
|
104
|
+
// Add find clause
|
|
105
|
+
this.findClause.forEach(item => {
|
|
106
|
+
if (typeof item === 'object' && item && item._type === 'symbol') {
|
|
107
|
+
query.push(item);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Aggregation function
|
|
111
|
+
const agg = item;
|
|
112
|
+
// Build aggregation as a list: (count ?e)
|
|
113
|
+
const aggList = [sym(agg.function), agg.variable];
|
|
114
|
+
query.push(agg.alias || aggList);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// Add in clause
|
|
118
|
+
if (this.inClause.length > 0) {
|
|
119
|
+
query.push(kw(":in"), ...this.inClause);
|
|
120
|
+
}
|
|
121
|
+
// Add with clause
|
|
122
|
+
if (this.withClause.length > 0) {
|
|
123
|
+
query.push(kw(":with"), ...this.withClause);
|
|
124
|
+
}
|
|
125
|
+
// Add where clause
|
|
126
|
+
if (this.whereClause.length > 0) {
|
|
127
|
+
query.push(kw(":where"));
|
|
128
|
+
this.whereClause.forEach(clause => {
|
|
129
|
+
// Convert QueryPattern to array format
|
|
130
|
+
const clauseArray = [clause.entity, clause.attribute, clause.value];
|
|
131
|
+
if (clause.tx)
|
|
132
|
+
clauseArray.push(clause.tx);
|
|
133
|
+
query.push(clauseArray);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return query;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Execute the query directly (requires database context)
|
|
140
|
+
*/
|
|
141
|
+
async execute(db, ...args) {
|
|
142
|
+
return db.query(this.build(), ...args);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Helper class for fluent entity-based query building
|
|
147
|
+
*/
|
|
148
|
+
export class EntityQueryBuilder {
|
|
149
|
+
constructor(queryBuilder, entity) {
|
|
150
|
+
this.queryBuilder = queryBuilder;
|
|
151
|
+
this.entity = entity;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Add an attribute to the current entity
|
|
155
|
+
* @example
|
|
156
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name'))
|
|
157
|
+
*/
|
|
158
|
+
hasAttribute(attribute) {
|
|
159
|
+
return new AttributeQueryBuilder(this.queryBuilder, this.entity, attribute);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Return to the main query builder
|
|
163
|
+
*/
|
|
164
|
+
build() {
|
|
165
|
+
return this.queryBuilder.build();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Helper class for fluent attribute-based query building
|
|
170
|
+
*/
|
|
171
|
+
export class AttributeQueryBuilder {
|
|
172
|
+
constructor(queryBuilder, entity, attribute) {
|
|
173
|
+
this.queryBuilder = queryBuilder;
|
|
174
|
+
this.entity = entity;
|
|
175
|
+
this.attribute = attribute;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Add a value constraint to the attribute
|
|
179
|
+
* @example
|
|
180
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).withValue(sym('?name'))
|
|
181
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/age')).withValue(25)
|
|
182
|
+
*/
|
|
183
|
+
withValue(value) {
|
|
184
|
+
this.queryBuilder.where({ entity: this.entity, attribute: this.attribute, value });
|
|
185
|
+
return this.queryBuilder;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Add the attribute without a specific value constraint (will bind to a variable)
|
|
189
|
+
* @example
|
|
190
|
+
* .entity(sym('?e')).hasAttribute(kw(':person/name')).bind()
|
|
191
|
+
*/
|
|
192
|
+
bind(variable) {
|
|
193
|
+
const value = variable || this.generateVariable();
|
|
194
|
+
this.queryBuilder.where({ entity: this.entity, attribute: this.attribute, value });
|
|
195
|
+
return this.queryBuilder;
|
|
196
|
+
}
|
|
197
|
+
generateVariable() {
|
|
198
|
+
const attrName = this.attribute.value.split('/').pop()?.replace(/[^a-zA-Z0-9]/g, '');
|
|
199
|
+
return sym(`?${attrName}`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Return to the main query builder
|
|
203
|
+
*/
|
|
204
|
+
build() {
|
|
205
|
+
return this.queryBuilder.build();
|
|
206
|
+
}
|
|
207
|
+
}
|