@nxgt/janus-drizzle 0.3.2 → 0.3.3

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.
Files changed (44) hide show
  1. package/dist/index.js +372 -301
  2. package/dist/index.js.map +20 -6
  3. package/dist/relations/context.d.ts +9 -0
  4. package/dist/relations/context.d.ts.map +1 -0
  5. package/dist/{relations.d.ts → relations/index.d.ts} +3 -3
  6. package/dist/relations/index.d.ts.map +1 -0
  7. package/dist/relations/reads.d.ts +9 -0
  8. package/dist/relations/reads.d.ts.map +1 -0
  9. package/dist/relations/tuples.d.ts +12 -0
  10. package/dist/relations/tuples.d.ts.map +1 -0
  11. package/dist/relations/writes.d.ts +7 -0
  12. package/dist/relations/writes.d.ts.map +1 -0
  13. package/dist/stores/identity-tables.d.ts +5 -0
  14. package/dist/stores/identity-tables.d.ts.map +1 -0
  15. package/dist/{stores.d.ts → stores/index.d.ts} +4 -6
  16. package/dist/stores/index.d.ts.map +1 -0
  17. package/dist/stores/logins.d.ts +13 -0
  18. package/dist/stores/logins.d.ts.map +1 -0
  19. package/dist/stores/records.d.ts +23 -0
  20. package/dist/stores/records.d.ts.map +1 -0
  21. package/dist/stores/sessions.d.ts +5 -0
  22. package/dist/stores/sessions.d.ts.map +1 -0
  23. package/dist/stores/tokens.d.ts +5 -0
  24. package/dist/stores/tokens.d.ts.map +1 -0
  25. package/dist/stores/users.d.ts +5 -0
  26. package/dist/stores/users.d.ts.map +1 -0
  27. package/dist/tables/columns.d.ts +22 -0
  28. package/dist/tables/columns.d.ts.map +1 -0
  29. package/dist/{tables.d.ts → tables/index.d.ts} +1 -1
  30. package/dist/tables/index.d.ts.map +1 -0
  31. package/dist/tables/logins.d.ts +74 -0
  32. package/dist/tables/logins.d.ts.map +1 -0
  33. package/dist/tables/relations.d.ts +133 -0
  34. package/dist/tables/relations.d.ts.map +1 -0
  35. package/dist/tables/sessions.d.ts +131 -0
  36. package/dist/tables/sessions.d.ts.map +1 -0
  37. package/dist/tables/tokens.d.ts +153 -0
  38. package/dist/tables/tokens.d.ts.map +1 -0
  39. package/dist/tables/users.d.ts +261 -0
  40. package/dist/tables/users.d.ts.map +1 -0
  41. package/package.json +3 -3
  42. package/dist/relations.d.ts.map +0 -1
  43. package/dist/stores.d.ts.map +0 -1
  44. package/dist/tables.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,58 +1,19 @@
1
- // src/relations.ts
2
- import { withTransaction } from "@nxgt/drizzle/pg";
3
- import {
4
- isSubjectSet
5
- } from "@nxgt/janus";
6
- import { and, asc, eq, gt, isNotNull, isNull, or } from "drizzle-orm";
1
+ // src/tables/index.ts
2
+ import { pgTable } from "drizzle-orm/pg-core";
7
3
 
8
- // src/tables.ts
9
- import { sql } from "drizzle-orm";
10
- import {
11
- boolean,
12
- check,
13
- customType,
14
- foreignKey,
15
- index,
16
- integer,
17
- jsonb,
18
- pgTable,
19
- primaryKey,
20
- text,
21
- timestamp,
22
- unique
23
- } from "drizzle-orm/pg-core";
4
+ // src/tables/logins.ts
5
+ import { foreignKey, index, primaryKey } from "drizzle-orm/pg-core";
6
+
7
+ // src/tables/columns.ts
8
+ import { customType, timestamp } from "drizzle-orm/pg-core";
24
9
  var key = customType({
25
10
  dataType: () => 'text collate "C"'
26
11
  });
27
12
  var at = (name) => timestamp(name, { withTimezone: true, precision: 3 });
28
- function defineJanusTables(options = {}) {
29
- const { schema } = options;
30
- const table = schema === undefined ? pgTable : schema.table;
31
- const users = table("users", {
32
- id: key("id").primaryKey(),
33
- type: key("type").notNull(),
34
- schemaVersion: text("schema_version").notNull(),
35
- active: boolean("active").notNull(),
36
- fields: jsonb("fields").notNull(),
37
- logins: text("logins").array().notNull(),
38
- passwordHash: text("password_hash"),
39
- passwordUpdatedAt: at("password_updated_at"),
40
- secondFactorMethod: text("second_factor_method", { enum: ["totp"] }),
41
- secondFactorSecret: text("second_factor_secret"),
42
- secondFactorConfirmedAt: at("second_factor_confirmed_at"),
43
- secondFactorLastStep: integer("second_factor_last_step"),
44
- emailVerifiedAt: at("email_verified_at"),
45
- version: integer("version").notNull(),
46
- createdAt: at("created_at").notNull(),
47
- updatedAt: at("updated_at").notNull()
48
- }, (t) => [
49
- unique("users_id_type_unique").on(t.id, t.type),
50
- index("users_type_id").on(t.type, t.id),
51
- check("users_password_whole", sql`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`),
52
- check("users_second_factor_whole", sql`(${t.secondFactorMethod} is null) = (${t.secondFactorSecret} is null) and (${t.secondFactorMethod} is not null or (${t.secondFactorConfirmedAt} is null and ${t.secondFactorLastStep} is null))`),
53
- check("users_second_factor_values", sql`${t.secondFactorMethod} in ('totp') and ${t.secondFactorLastStep} >= 0`)
54
- ]);
55
- const logins = table("logins", {
13
+
14
+ // src/tables/logins.ts
15
+ function loginsTable(table, users) {
16
+ return table("logins", {
56
17
  type: key("type").notNull(),
57
18
  login: key("login").notNull(),
58
19
  userId: key("user_id").notNull()
@@ -65,7 +26,28 @@ function defineJanusTables(options = {}) {
65
26
  }).onDelete("cascade"),
66
27
  index("logins_user_id").on(t.userId)
67
28
  ]);
68
- const sessions = table("sessions", {
29
+ }
30
+
31
+ // src/tables/relations.ts
32
+ import { index as index2, unique } from "drizzle-orm/pg-core";
33
+ function relationsTable(table) {
34
+ return table("relations", {
35
+ objectType: key("object_type").notNull(),
36
+ objectId: key("object_id").notNull(),
37
+ relation: key("relation").notNull(),
38
+ subjectType: key("subject_type").notNull(),
39
+ subjectId: key("subject_id").notNull(),
40
+ subjectRelation: key("subject_relation")
41
+ }, (t) => [
42
+ unique("relations_tuple_unique").on(t.subjectType, t.subjectId, t.subjectRelation, t.relation, t.objectType, t.objectId).nullsNotDistinct(),
43
+ index2("relations_object").on(t.objectType, t.objectId, t.relation)
44
+ ]);
45
+ }
46
+
47
+ // src/tables/sessions.ts
48
+ import { index as index3, unique as unique2 } from "drizzle-orm/pg-core";
49
+ function sessionsTable(table) {
50
+ return table("sessions", {
69
51
  id: key("id").primaryKey(),
70
52
  tokenHash: key("token_hash").notNull(),
71
53
  userId: key("user_id").notNull(),
@@ -74,11 +56,17 @@ function defineJanusTables(options = {}) {
74
56
  revokedAt: at("revoked_at"),
75
57
  createdAt: at("created_at").notNull()
76
58
  }, (t) => [
77
- unique("sessions_token_hash_unique").on(t.tokenHash),
78
- index("sessions_user_id").on(t.userId),
79
- index("sessions_expires_at").on(t.expiresAt)
59
+ unique2("sessions_token_hash_unique").on(t.tokenHash),
60
+ index3("sessions_user_id").on(t.userId),
61
+ index3("sessions_expires_at").on(t.expiresAt)
80
62
  ]);
81
- const tokens = table("tokens", {
63
+ }
64
+
65
+ // src/tables/tokens.ts
66
+ import { sql } from "drizzle-orm";
67
+ import { check, index as index4, integer, text } from "drizzle-orm/pg-core";
68
+ function tokensTable(table) {
69
+ return table("tokens", {
82
70
  tokenHash: key("token_hash").primaryKey(),
83
71
  kind: text("kind", {
84
72
  enum: ["verifyEmail", "resetPassword", "secondFactor", "signInCode"]
@@ -93,19 +81,57 @@ function defineJanusTables(options = {}) {
93
81
  }, (t) => [
94
82
  check("tokens_kind", sql`${t.kind} in ('verifyEmail', 'resetPassword', 'secondFactor', 'signInCode')`),
95
83
  check("tokens_attempts", sql`${t.attempts} >= 0`),
96
- index("tokens_user_id").on(t.userId)
84
+ index4("tokens_user_id").on(t.userId)
97
85
  ]);
98
- const relations = table("relations", {
99
- objectType: key("object_type").notNull(),
100
- objectId: key("object_id").notNull(),
101
- relation: key("relation").notNull(),
102
- subjectType: key("subject_type").notNull(),
103
- subjectId: key("subject_id").notNull(),
104
- subjectRelation: key("subject_relation")
86
+ }
87
+
88
+ // src/tables/users.ts
89
+ import { sql as sql2 } from "drizzle-orm";
90
+ import {
91
+ boolean,
92
+ check as check2,
93
+ index as index5,
94
+ integer as integer2,
95
+ jsonb,
96
+ text as text2,
97
+ unique as unique3
98
+ } from "drizzle-orm/pg-core";
99
+ function usersTable(table) {
100
+ return table("users", {
101
+ id: key("id").primaryKey(),
102
+ type: key("type").notNull(),
103
+ schemaVersion: text2("schema_version").notNull(),
104
+ active: boolean("active").notNull(),
105
+ fields: jsonb("fields").notNull(),
106
+ logins: text2("logins").array().notNull(),
107
+ passwordHash: text2("password_hash"),
108
+ passwordUpdatedAt: at("password_updated_at"),
109
+ secondFactorMethod: text2("second_factor_method", { enum: ["totp"] }),
110
+ secondFactorSecret: text2("second_factor_secret"),
111
+ secondFactorConfirmedAt: at("second_factor_confirmed_at"),
112
+ secondFactorLastStep: integer2("second_factor_last_step"),
113
+ emailVerifiedAt: at("email_verified_at"),
114
+ version: integer2("version").notNull(),
115
+ createdAt: at("created_at").notNull(),
116
+ updatedAt: at("updated_at").notNull()
105
117
  }, (t) => [
106
- unique("relations_tuple_unique").on(t.subjectType, t.subjectId, t.subjectRelation, t.relation, t.objectType, t.objectId).nullsNotDistinct(),
107
- index("relations_object").on(t.objectType, t.objectId, t.relation)
118
+ unique3("users_id_type_unique").on(t.id, t.type),
119
+ index5("users_type_id").on(t.type, t.id),
120
+ check2("users_password_whole", sql2`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`),
121
+ check2("users_second_factor_whole", sql2`(${t.secondFactorMethod} is null) = (${t.secondFactorSecret} is null) and (${t.secondFactorMethod} is not null or (${t.secondFactorConfirmedAt} is null and ${t.secondFactorLastStep} is null))`),
122
+ check2("users_second_factor_values", sql2`${t.secondFactorMethod} in ('totp') and ${t.secondFactorLastStep} >= 0`)
108
123
  ]);
124
+ }
125
+
126
+ // src/tables/index.ts
127
+ function defineJanusTables(options = {}) {
128
+ const { schema } = options;
129
+ const table = schema === undefined ? pgTable : schema.table;
130
+ const users = usersTable(table);
131
+ const logins = loginsTable(table, users);
132
+ const sessions = sessionsTable(table);
133
+ const tokens = tokensTable(table);
134
+ const relations = relationsTable(table);
109
135
  return { users, logins, sessions, tokens, relations };
110
136
  }
111
137
 
@@ -129,62 +155,12 @@ function loginTaken(operation, type, login) {
129
155
  return new StoreConflict("login", `${operation}: the login is taken by another ${type}`, { login, userType: type, operation });
130
156
  }
131
157
 
132
- // src/relations.ts
133
- function createDrizzleRelations(db, options = {}) {
134
- const tables = options.tables ?? defineJanusTables();
135
- const { rowOf, subjectIs, matching } = tupleSql(tables.relations);
136
- const run$ = (operation, body) => run("relations", operation, body);
137
- const holding = (object, relation, sets) => db.select({
138
- type: tables.relations.subjectType,
139
- id: tables.relations.subjectId,
140
- relation: tables.relations.subjectRelation
141
- }).from(tables.relations).where(and(eq(tables.relations.objectType, object.type), eq(tables.relations.objectId, object.id), eq(tables.relations.relation, relation), sets ? isNotNull(tables.relations.subjectRelation) : isNull(tables.relations.subjectRelation)));
142
- const apply = async (tx, add, remove) => {
143
- for (const tuple of inOrder(remove)) {
144
- await tx.delete(tables.relations).where(matching(tuple));
145
- }
146
- if (add.length > 0) {
147
- await tx.insert(tables.relations).values(inOrder(add).map(rowOf)).onConflictDoNothing();
148
- }
149
- };
150
- return {
151
- write: ({ add = [], remove = [] }) => run$("write", async () => {
152
- if (add.length + remove.length <= 1)
153
- return apply(db, add, remove);
154
- await withTransaction(db, (tx) => apply(tx, add, remove));
155
- }),
156
- has: (tuple) => run$("has", async () => {
157
- const found = await db.select({ relation: tables.relations.relation }).from(tables.relations).where(matching(tuple)).limit(1);
158
- return found.length === 1;
159
- }),
160
- findSubjectSets: (object, relation) => run$("findSubjectSets", async () => (await holding(object, relation, true)).map((row) => ({
161
- type: row.type,
162
- id: row.id,
163
- relation: row.relation ?? ""
164
- }))),
165
- findEntities: (object, relation) => run$("findEntities", async () => (await holding(object, relation, false)).map((row) => ({ type: row.type, id: row.id }))),
166
- findObjects: ({ type, relation, subject, after, limit }) => run$("findObjects", async () => {
167
- const conditions = [
168
- subjectIs(subject),
169
- eq(tables.relations.relation, relation),
170
- eq(tables.relations.objectType, type)
171
- ];
172
- if (after !== null)
173
- conditions.push(gt(tables.relations.objectId, after));
174
- const found = await db.select({ id: tables.relations.objectId }).from(tables.relations).where(and(...conditions)).orderBy(asc(tables.relations.objectId)).limit(limit + 1);
175
- const items = found.slice(0, limit).map((row) => row.id);
176
- const last = items.at(-1);
177
- return {
178
- items,
179
- nextCursor: found.length > limit && last !== undefined ? last : null
180
- };
181
- }),
182
- deleteEntity: (entity) => run$("deleteEntity", async () => {
183
- const deleted = await db.delete(tables.relations).where(or(and(eq(tables.relations.objectType, entity.type), eq(tables.relations.objectId, entity.id)), and(eq(tables.relations.subjectType, entity.type), eq(tables.relations.subjectId, entity.id)))).returning({ relation: tables.relations.relation });
184
- return deleted.length;
185
- })
186
- };
187
- }
158
+ // src/relations/reads.ts
159
+ import { and as and2, asc, eq as eq2, gt, isNotNull, isNull as isNull2 } from "drizzle-orm";
160
+
161
+ // src/relations/tuples.ts
162
+ import { isSubjectSet } from "@nxgt/janus";
163
+ import { and, eq, isNull } from "drizzle-orm";
188
164
  function inOrder(tuples) {
189
165
  const keyOf = (tuple) => JSON.stringify([
190
166
  tuple.object.type,
@@ -199,194 +175,103 @@ function inOrder(tuples) {
199
175
  return x < y ? -1 : x > y ? 1 : 0;
200
176
  });
201
177
  }
202
- function tupleSql(relations) {
203
- function rowOf(tuple) {
204
- return {
205
- objectType: tuple.object.type,
206
- objectId: tuple.object.id,
207
- relation: tuple.relation,
208
- subjectType: tuple.subject.type,
209
- subjectId: tuple.subject.id,
210
- subjectRelation: isSubjectSet(tuple.subject) ? tuple.subject.relation : null
211
- };
212
- }
213
- function subjectIs(subject) {
214
- return and(eq(relations.subjectType, subject.type), eq(relations.subjectId, subject.id), isSubjectSet(subject) ? eq(relations.subjectRelation, subject.relation) : isNull(relations.subjectRelation));
215
- }
216
- function matching(tuple) {
217
- return and(eq(relations.objectType, tuple.object.type), eq(relations.objectId, tuple.object.id), eq(relations.relation, tuple.relation), subjectIs(tuple.subject));
218
- }
219
- return { rowOf, subjectIs, matching };
220
- }
221
-
222
- // src/stores.ts
223
- import { withTransaction as withTransaction2 } from "@nxgt/drizzle/pg";
224
- import { NotFoundError, StoreConflict as StoreConflict2 } from "@nxgt/janus";
225
- import { and as and2, asc as asc2, eq as eq2, gt as gt2, isNull as isNull2, lte, ne, sql as sql2 } from "drizzle-orm";
226
- function createDrizzleStores(db, options = {}) {
227
- const tables = options.tables ?? defineJanusTables();
178
+ function rowOf(tuple) {
228
179
  return {
229
- users: userStore(db, tables),
230
- sessions: sessionStore(db, tables),
231
- tokens: tokenStore(db, tables)
180
+ objectType: tuple.object.type,
181
+ objectId: tuple.object.id,
182
+ relation: tuple.relation,
183
+ subjectType: tuple.subject.type,
184
+ subjectId: tuple.subject.id,
185
+ subjectRelation: isSubjectSet(tuple.subject) ? tuple.subject.relation : null
232
186
  };
233
187
  }
234
- function userStore(db, tables) {
235
- const run$ = (operation, body) => run("users", operation, body);
236
- const find = async (id) => {
237
- const [row] = await db.select().from(tables.users).where(eq2(tables.users.id, id));
238
- return row === undefined ? null : toUser(row);
239
- };
240
- const claim = async (tx, operation, user, logins) => {
241
- const distinct = [...new Set(logins)].sort();
242
- if (distinct.length === 0)
243
- return;
244
- const written = await tx.insert(tables.logins).values(distinct.map((login) => ({ type: user.type, login, userId: user.id }))).onConflictDoNothing().returning({ login: tables.logins.login });
245
- if (written.length === distinct.length)
246
- return;
247
- const claimed = new Set(written.map((row) => row.login));
248
- const taken = distinct.find((login) => !claimed.has(login)) ?? "";
249
- throw loginTaken(operation, user.type, taken);
250
- };
251
- return {
252
- insertUser: (record) => run$("insertUser", async () => {
253
- const inserted = await withTransaction2(db, async (tx) => {
254
- const [row] = await tx.insert(tables.users).values(toUserRow(record)).onConflictDoNothing({ target: tables.users.id }).returning();
255
- if (row === undefined)
256
- return null;
257
- await claim(tx, "insertUser", record, record.logins);
258
- return toUser(row);
259
- });
260
- if (inserted !== null)
261
- return inserted;
262
- const stored = await find(record.id);
263
- if (stored !== null)
264
- return stored;
265
- throw new NotFoundError("insertUser: the user was deleted meanwhile", {
266
- userId: record.id,
267
- operation: "insertUser"
268
- });
269
- }),
270
- findUser: (id) => run$("findUser", () => find(id)),
271
- findUserByLogin: (type, login) => run$("findUserByLogin", async () => {
272
- const [row] = await db.select({ user: tables.users }).from(tables.logins).innerJoin(tables.users, eq2(tables.users.id, tables.logins.userId)).where(and2(eq2(tables.logins.type, type), eq2(tables.logins.login, login)));
273
- return row === undefined ? null : toUser(row.user);
274
- }),
275
- listUsers: ({ type, after, limit }) => run$("listUsers", async () => {
276
- const found = await db.select().from(tables.users).where(after === null ? eq2(tables.users.type, type) : and2(eq2(tables.users.type, type), gt2(tables.users.id, after))).orderBy(asc2(tables.users.id)).limit(limit + 1);
277
- const items = found.slice(0, limit).map(toUser);
278
- const last = items.at(-1);
279
- return {
280
- items,
281
- nextCursor: found.length > limit && last !== undefined ? last.id : null
282
- };
283
- }),
284
- updateUser: (id, patch, ifVersion) => run$("updateUser", () => withTransaction2(db, async (tx) => {
285
- const [row] = await tx.update(tables.users).set({
286
- ...toUserSet(patch),
287
- version: sql2`${tables.users.version} + 1`
288
- }).where(and2(eq2(tables.users.id, id), eq2(tables.users.version, ifVersion))).returning();
289
- if (row === undefined) {
290
- const [stored] = await tx.select({ version: tables.users.version }).from(tables.users).where(eq2(tables.users.id, id));
291
- if (stored === undefined) {
292
- throw new NotFoundError("updateUser: no user has this id", {
293
- userId: id,
294
- operation: "updateUser"
295
- });
296
- }
297
- throw new StoreConflict2("version", `updateUser: expected version ${ifVersion}, found ${stored.version}`, {
298
- userId: id,
299
- expectedVersion: ifVersion,
300
- actualVersion: stored.version,
301
- operation: "updateUser"
302
- });
303
- }
304
- if (patch.logins !== undefined) {
305
- await tx.delete(tables.logins).where(eq2(tables.logins.userId, id));
306
- await claim(tx, "updateUser", row, patch.logins);
307
- }
308
- return toUser(row);
309
- })),
310
- deleteUser: (id) => run$("deleteUser", async () => {
311
- const deleted = await db.delete(tables.users).where(eq2(tables.users.id, id)).returning({ id: tables.users.id });
312
- return deleted.length === 1;
313
- })
314
- };
188
+ function subjectIs(relations, subject) {
189
+ return and(eq(relations.subjectType, subject.type), eq(relations.subjectId, subject.id), isSubjectSet(subject) ? eq(relations.subjectRelation, subject.relation) : isNull(relations.subjectRelation));
315
190
  }
316
- function sessionStore(db, tables) {
317
- const run$ = (operation, body) => run("sessions", operation, body);
191
+ function matching(relations, tuple) {
192
+ return and(eq(relations.objectType, tuple.object.type), eq(relations.objectId, tuple.object.id), eq(relations.relation, tuple.relation), subjectIs(relations, tuple.subject));
193
+ }
194
+
195
+ // src/relations/reads.ts
196
+ async function has({ db, relations }, tuple) {
197
+ const found = await db.select({ relation: relations.relation }).from(relations).where(matching(relations, tuple)).limit(1);
198
+ return found.length === 1;
199
+ }
200
+ async function findSubjectSets(context, object, relation) {
201
+ return (await holding(context, object, relation, true)).map((row) => ({
202
+ type: row.type,
203
+ id: row.id,
204
+ relation: row.relation ?? ""
205
+ }));
206
+ }
207
+ async function findEntities(context, object, relation) {
208
+ return (await holding(context, object, relation, false)).map((row) => ({ type: row.type, id: row.id }));
209
+ }
210
+ function holding({ db, relations }, object, relation, sets) {
211
+ return db.select({
212
+ type: relations.subjectType,
213
+ id: relations.subjectId,
214
+ relation: relations.subjectRelation
215
+ }).from(relations).where(and2(eq2(relations.objectType, object.type), eq2(relations.objectId, object.id), eq2(relations.relation, relation), sets ? isNotNull(relations.subjectRelation) : isNull2(relations.subjectRelation)));
216
+ }
217
+ async function findObjects({ db, relations }, { type, relation, subject, after, limit }) {
218
+ const conditions = [
219
+ subjectIs(relations, subject),
220
+ eq2(relations.relation, relation),
221
+ eq2(relations.objectType, type)
222
+ ];
223
+ if (after !== null)
224
+ conditions.push(gt(relations.objectId, after));
225
+ const found = await db.select({ id: relations.objectId }).from(relations).where(and2(...conditions)).orderBy(asc(relations.objectId)).limit(limit + 1);
226
+ const items = found.slice(0, limit).map((row) => row.id);
227
+ const last = items.at(-1);
318
228
  return {
319
- insertSession: (record) => run$("insertSession", async () => {
320
- await db.insert(tables.sessions).values(record).onConflictDoNothing({ target: tables.sessions.id });
321
- }),
322
- findSessionByTokenHash: (tokenHash) => run$("findSessionByTokenHash", async () => {
323
- const [row] = await db.select().from(tables.sessions).where(eq2(tables.sessions.tokenHash, tokenHash));
324
- return row === undefined ? null : toSession(row);
325
- }),
326
- extendSession: (id, expiresAt) => run$("extendSession", async () => {
327
- const [row] = await db.update(tables.sessions).set({ expiresAt }).where(and2(eq2(tables.sessions.id, id), isNull2(tables.sessions.revokedAt))).returning();
328
- return row === undefined ? null : toSession(row);
329
- }),
330
- revokeSession: (id, at) => run$("revokeSession", async () => {
331
- const matched = await db.update(tables.sessions).set({
332
- revokedAt: sql2`coalesce(${tables.sessions.revokedAt}, ${stamp(at)})`
333
- }).where(eq2(tables.sessions.id, id)).returning({ id: tables.sessions.id });
334
- return matched.length === 1;
335
- }),
336
- revokeUserSessions: (userId, at, except) => run$("revokeUserSessions", async () => {
337
- const standing = and2(eq2(tables.sessions.userId, userId), isNull2(tables.sessions.revokedAt));
338
- const revoked = await db.update(tables.sessions).set({ revokedAt: at }).where(except === undefined ? standing : and2(standing, ne(tables.sessions.id, except))).returning({ id: tables.sessions.id });
339
- return revoked.length;
340
- }),
341
- deleteUserSessions: (userId) => run$("deleteUserSessions", async () => {
342
- const deleted = await db.delete(tables.sessions).where(eq2(tables.sessions.userId, userId)).returning({ id: tables.sessions.id });
343
- return deleted.length;
344
- }),
345
- deleteExpiredSessions: (before) => run$("deleteExpiredSessions", async () => {
346
- const deleted = await db.delete(tables.sessions).where(lte(tables.sessions.expiresAt, before)).returning({ id: tables.sessions.id });
347
- return deleted.length;
348
- })
229
+ items,
230
+ nextCursor: found.length > limit && last !== undefined ? last : null
349
231
  };
350
232
  }
351
- function tokenStore(db, tables) {
352
- const run$ = (operation, body) => run("tokens", operation, body);
233
+
234
+ // src/relations/writes.ts
235
+ import { withTransaction } from "@nxgt/drizzle/pg";
236
+ import { and as and3, eq as eq3, or } from "drizzle-orm";
237
+ async function write({ db, relations }, add, remove) {
238
+ if (add.length + remove.length <= 1)
239
+ return apply(db, relations, add, remove);
240
+ await withTransaction(db, (tx) => apply(tx, relations, add, remove));
241
+ }
242
+ async function apply(tx, relations, add, remove) {
243
+ for (const tuple of inOrder(remove)) {
244
+ await tx.delete(relations).where(matching(relations, tuple));
245
+ }
246
+ if (add.length > 0) {
247
+ await tx.insert(relations).values(inOrder(add).map(rowOf)).onConflictDoNothing();
248
+ }
249
+ }
250
+ async function deleteEntity({ db, relations }, entity) {
251
+ const deleted = await db.delete(relations).where(or(and3(eq3(relations.objectType, entity.type), eq3(relations.objectId, entity.id)), and3(eq3(relations.subjectType, entity.type), eq3(relations.subjectId, entity.id)))).returning({ relation: relations.relation });
252
+ return deleted.length;
253
+ }
254
+
255
+ // src/relations/index.ts
256
+ function createDrizzleRelations(db, options = {}) {
257
+ const tables = options.tables ?? defineJanusTables();
258
+ const context = { db, relations: tables.relations };
259
+ const run$ = (operation, body) => run("relations", operation, body);
353
260
  return {
354
- insertToken: (record) => run$("insertToken", async () => {
355
- await db.insert(tables.tokens).values(record).onConflictDoNothing();
356
- }),
357
- consumeToken: (tokenHash, kind, at) => run$("consumeToken", async () => {
358
- const before = db.$with("before").as(db.select().from(tables.tokens).where(and2(eq2(tables.tokens.tokenHash, tokenHash), eq2(tables.tokens.kind, kind))).for("update"));
359
- const [spent] = await db.with(before).update(tables.tokens).set({
360
- spentAt: sql2`coalesce(${tables.tokens.spentAt}, ${stamp(at)})`
361
- }).from(before).where(eq2(tables.tokens.tokenHash, before.tokenHash)).returning({
362
- tokenHash: before.tokenHash,
363
- kind: before.kind,
364
- userId: before.userId,
365
- address: before.address,
366
- codeHash: before.codeHash,
367
- attempts: before.attempts,
368
- expiresAt: before.expiresAt,
369
- spentAt: before.spentAt,
370
- createdAt: before.createdAt
371
- });
372
- return spent === undefined ? null : { ...spent, userId: spent.userId };
373
- }),
374
- countAttempt: (tokenHash, kind) => run$("countAttempt", async () => {
375
- const match = and2(eq2(tables.tokens.tokenHash, tokenHash), eq2(tables.tokens.kind, kind));
376
- const [counted] = await db.update(tables.tokens).set({ attempts: sql2`${tables.tokens.attempts} + 1` }).where(and2(match, isNull2(tables.tokens.spentAt))).returning();
377
- const row = counted ?? (await db.select().from(tables.tokens).where(match))[0];
378
- return row === undefined ? null : { ...row, userId: row.userId };
379
- }),
380
- spendUserTokens: (userId, kind, at, except) => run$("spendUserTokens", async () => {
381
- const spent = await db.update(tables.tokens).set({ spentAt: at }).where(and2(eq2(tables.tokens.userId, userId), eq2(tables.tokens.kind, kind), isNull2(tables.tokens.spentAt), except === undefined ? undefined : ne(tables.tokens.tokenHash, except))).returning({ tokenHash: tables.tokens.tokenHash });
382
- return spent.length;
383
- }),
384
- deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
385
- const deleted = await db.delete(tables.tokens).where(eq2(tables.tokens.userId, userId)).returning({ tokenHash: tables.tokens.tokenHash });
386
- return deleted.length;
387
- })
261
+ write: ({ add = [], remove = [] }) => run$("write", () => write(context, add, remove)),
262
+ has: (tuple) => run$("has", () => has(context, tuple)),
263
+ findSubjectSets: (object, relation) => run$("findSubjectSets", () => findSubjectSets(context, object, relation)),
264
+ findEntities: (object, relation) => run$("findEntities", () => findEntities(context, object, relation)),
265
+ findObjects: ({ type, relation, subject, after, limit }) => run$("findObjects", () => findObjects(context, { type, relation, subject, after, limit })),
266
+ deleteEntity: (entity) => run$("deleteEntity", () => deleteEntity(context, entity))
388
267
  };
389
268
  }
269
+
270
+ // src/stores/sessions.ts
271
+ import { and as and4, eq as eq4, isNull as isNull3, lte, ne, sql as sql4 } from "drizzle-orm";
272
+
273
+ // src/stores/records.ts
274
+ import { sql as sql3 } from "drizzle-orm";
390
275
  function toUserRow(record) {
391
276
  return {
392
277
  id: record.id,
@@ -468,7 +353,193 @@ function toSession(row) {
468
353
  };
469
354
  }
470
355
  function stamp(at) {
471
- return sql2`${at.toISOString()}::timestamptz`;
356
+ return sql3`${at.toISOString()}::timestamptz`;
357
+ }
358
+
359
+ // src/stores/sessions.ts
360
+ function sessionStore(db, tables) {
361
+ const run$ = (operation, body) => run("sessions", operation, body);
362
+ return {
363
+ insertSession: (record) => run$("insertSession", async () => {
364
+ await db.insert(tables.sessions).values(record).onConflictDoNothing({ target: tables.sessions.id });
365
+ }),
366
+ findSessionByTokenHash: (tokenHash) => run$("findSessionByTokenHash", async () => {
367
+ const [row] = await db.select().from(tables.sessions).where(eq4(tables.sessions.tokenHash, tokenHash));
368
+ return row === undefined ? null : toSession(row);
369
+ }),
370
+ extendSession: (id, expiresAt) => run$("extendSession", async () => {
371
+ const [row] = await db.update(tables.sessions).set({ expiresAt }).where(and4(eq4(tables.sessions.id, id), isNull3(tables.sessions.revokedAt))).returning();
372
+ return row === undefined ? null : toSession(row);
373
+ }),
374
+ revokeSession: (id, at) => run$("revokeSession", () => revokeSession(db, tables, id, at)),
375
+ revokeUserSessions: (userId, at, except) => run$("revokeUserSessions", () => revokeUserSessions(db, tables, userId, at, except)),
376
+ deleteUserSessions: (userId) => run$("deleteUserSessions", async () => {
377
+ const deleted = await db.delete(tables.sessions).where(eq4(tables.sessions.userId, userId)).returning({ id: tables.sessions.id });
378
+ return deleted.length;
379
+ }),
380
+ deleteExpiredSessions: (before) => run$("deleteExpiredSessions", async () => {
381
+ const deleted = await db.delete(tables.sessions).where(lte(tables.sessions.expiresAt, before)).returning({ id: tables.sessions.id });
382
+ return deleted.length;
383
+ })
384
+ };
385
+ }
386
+ async function revokeSession(db, tables, id, at) {
387
+ const matched = await db.update(tables.sessions).set({
388
+ revokedAt: sql4`coalesce(${tables.sessions.revokedAt}, ${stamp(at)})`
389
+ }).where(eq4(tables.sessions.id, id)).returning({ id: tables.sessions.id });
390
+ return matched.length === 1;
391
+ }
392
+ async function revokeUserSessions(db, tables, userId, at, except) {
393
+ const standing = and4(eq4(tables.sessions.userId, userId), isNull3(tables.sessions.revokedAt));
394
+ const revoked = await db.update(tables.sessions).set({ revokedAt: at }).where(except === undefined ? standing : and4(standing, ne(tables.sessions.id, except))).returning({ id: tables.sessions.id });
395
+ return revoked.length;
396
+ }
397
+
398
+ // src/stores/tokens.ts
399
+ import { and as and5, eq as eq5, isNull as isNull4, ne as ne2, sql as sql5 } from "drizzle-orm";
400
+ function tokenStore(db, tables) {
401
+ const run$ = (operation, body) => run("tokens", operation, body);
402
+ return {
403
+ insertToken: (record) => run$("insertToken", async () => {
404
+ await db.insert(tables.tokens).values(record).onConflictDoNothing();
405
+ }),
406
+ consumeToken: (tokenHash, kind, at) => run$("consumeToken", () => consumeToken(db, tables, tokenHash, kind, at)),
407
+ countAttempt: (tokenHash, kind) => run$("countAttempt", () => countAttempt(db, tables, tokenHash, kind)),
408
+ spendUserTokens: (userId, kind, at, except) => run$("spendUserTokens", async () => {
409
+ const spent = await db.update(tables.tokens).set({ spentAt: at }).where(and5(eq5(tables.tokens.userId, userId), eq5(tables.tokens.kind, kind), isNull4(tables.tokens.spentAt), except === undefined ? undefined : ne2(tables.tokens.tokenHash, except))).returning({ tokenHash: tables.tokens.tokenHash });
410
+ return spent.length;
411
+ }),
412
+ deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
413
+ const deleted = await db.delete(tables.tokens).where(eq5(tables.tokens.userId, userId)).returning({ tokenHash: tables.tokens.tokenHash });
414
+ return deleted.length;
415
+ })
416
+ };
417
+ }
418
+ async function consumeToken(db, tables, tokenHash, kind, at) {
419
+ const before = db.$with("before").as(db.select().from(tables.tokens).where(and5(eq5(tables.tokens.tokenHash, tokenHash), eq5(tables.tokens.kind, kind))).for("update"));
420
+ const [spent] = await db.with(before).update(tables.tokens).set({
421
+ spentAt: sql5`coalesce(${tables.tokens.spentAt}, ${stamp(at)})`
422
+ }).from(before).where(eq5(tables.tokens.tokenHash, before.tokenHash)).returning({
423
+ tokenHash: before.tokenHash,
424
+ kind: before.kind,
425
+ userId: before.userId,
426
+ address: before.address,
427
+ codeHash: before.codeHash,
428
+ attempts: before.attempts,
429
+ expiresAt: before.expiresAt,
430
+ spentAt: before.spentAt,
431
+ createdAt: before.createdAt
432
+ });
433
+ return spent === undefined ? null : { ...spent, userId: spent.userId };
434
+ }
435
+ async function countAttempt(db, tables, tokenHash, kind) {
436
+ const match = and5(eq5(tables.tokens.tokenHash, tokenHash), eq5(tables.tokens.kind, kind));
437
+ const [counted] = await db.update(tables.tokens).set({ attempts: sql5`${tables.tokens.attempts} + 1` }).where(and5(match, isNull4(tables.tokens.spentAt))).returning();
438
+ const row = counted ?? (await db.select().from(tables.tokens).where(match))[0];
439
+ return row === undefined ? null : { ...row, userId: row.userId };
440
+ }
441
+
442
+ // src/stores/users.ts
443
+ import { withTransaction as withTransaction2 } from "@nxgt/drizzle/pg";
444
+ import { NotFoundError, StoreConflict as StoreConflict2 } from "@nxgt/janus";
445
+ import { and as and6, asc as asc2, eq as eq6, gt as gt2, sql as sql6 } from "drizzle-orm";
446
+
447
+ // src/stores/logins.ts
448
+ async function claimLogins(tx, tables, operation, user, logins) {
449
+ const distinct = [...new Set(logins)].sort();
450
+ if (distinct.length === 0)
451
+ return;
452
+ const written = await tx.insert(tables.logins).values(distinct.map((login) => ({ type: user.type, login, userId: user.id }))).onConflictDoNothing().returning({ login: tables.logins.login });
453
+ if (written.length === distinct.length)
454
+ return;
455
+ const claimed = new Set(written.map((row) => row.login));
456
+ const taken = distinct.find((login) => !claimed.has(login)) ?? "";
457
+ throw loginTaken(operation, user.type, taken);
458
+ }
459
+
460
+ // src/stores/users.ts
461
+ function userStore(db, tables) {
462
+ const run$ = (operation, body) => run("users", operation, body);
463
+ return {
464
+ insertUser: (record) => run$("insertUser", () => insertUser(db, tables, record)),
465
+ findUser: (id) => run$("findUser", () => findUser(db, tables, id)),
466
+ findUserByLogin: (type, login) => run$("findUserByLogin", async () => {
467
+ const [row] = await db.select({ user: tables.users }).from(tables.logins).innerJoin(tables.users, eq6(tables.users.id, tables.logins.userId)).where(and6(eq6(tables.logins.type, type), eq6(tables.logins.login, login)));
468
+ return row === undefined ? null : toUser(row.user);
469
+ }),
470
+ listUsers: ({ type, after, limit }) => run$("listUsers", async () => {
471
+ const found = await db.select().from(tables.users).where(after === null ? eq6(tables.users.type, type) : and6(eq6(tables.users.type, type), gt2(tables.users.id, after))).orderBy(asc2(tables.users.id)).limit(limit + 1);
472
+ const items = found.slice(0, limit).map(toUser);
473
+ const last = items.at(-1);
474
+ return {
475
+ items,
476
+ nextCursor: found.length > limit && last !== undefined ? last.id : null
477
+ };
478
+ }),
479
+ updateUser: (id, patch, ifVersion) => run$("updateUser", () => withTransaction2(db, (tx) => updateUser(tx, tables, id, patch, ifVersion))),
480
+ deleteUser: (id) => run$("deleteUser", async () => {
481
+ const deleted = await db.delete(tables.users).where(eq6(tables.users.id, id)).returning({ id: tables.users.id });
482
+ return deleted.length === 1;
483
+ })
484
+ };
485
+ }
486
+ async function findUser(db, tables, id) {
487
+ const [row] = await db.select().from(tables.users).where(eq6(tables.users.id, id));
488
+ return row === undefined ? null : toUser(row);
489
+ }
490
+ async function insertUser(db, tables, record) {
491
+ const inserted = await withTransaction2(db, async (tx) => {
492
+ const [row] = await tx.insert(tables.users).values(toUserRow(record)).onConflictDoNothing({ target: tables.users.id }).returning();
493
+ if (row === undefined)
494
+ return null;
495
+ await claimLogins(tx, tables, "insertUser", record, record.logins);
496
+ return toUser(row);
497
+ });
498
+ if (inserted !== null)
499
+ return inserted;
500
+ const stored = await findUser(db, tables, record.id);
501
+ if (stored !== null)
502
+ return stored;
503
+ throw new NotFoundError("insertUser: the user was deleted meanwhile", {
504
+ userId: record.id,
505
+ operation: "insertUser"
506
+ });
507
+ }
508
+ async function updateUser(tx, tables, id, patch, ifVersion) {
509
+ const [row] = await tx.update(tables.users).set({
510
+ ...toUserSet(patch),
511
+ version: sql6`${tables.users.version} + 1`
512
+ }).where(and6(eq6(tables.users.id, id), eq6(tables.users.version, ifVersion))).returning();
513
+ if (row === undefined) {
514
+ const [stored] = await tx.select({ version: tables.users.version }).from(tables.users).where(eq6(tables.users.id, id));
515
+ if (stored === undefined) {
516
+ throw new NotFoundError("updateUser: no user has this id", {
517
+ userId: id,
518
+ operation: "updateUser"
519
+ });
520
+ }
521
+ throw new StoreConflict2("version", `updateUser: expected version ${ifVersion}, found ${stored.version}`, {
522
+ userId: id,
523
+ expectedVersion: ifVersion,
524
+ actualVersion: stored.version,
525
+ operation: "updateUser"
526
+ });
527
+ }
528
+ if (patch.logins !== undefined) {
529
+ await tx.delete(tables.logins).where(eq6(tables.logins.userId, id));
530
+ await claimLogins(tx, tables, "updateUser", row, patch.logins);
531
+ }
532
+ return toUser(row);
533
+ }
534
+
535
+ // src/stores/index.ts
536
+ function createDrizzleStores(db, options = {}) {
537
+ const tables = options.tables ?? defineJanusTables();
538
+ return {
539
+ users: userStore(db, tables),
540
+ sessions: sessionStore(db, tables),
541
+ tokens: tokenStore(db, tables)
542
+ };
472
543
  }
473
544
 
474
545
  // src/adapter.ts
@@ -485,5 +556,5 @@ export {
485
556
  defineJanusTables
486
557
  };
487
558
 
488
- //# debugId=7AD52CBFCEA4D74A64756E2164756E21
559
+ //# debugId=3F6F0185055BC57C64756E2164756E21
489
560
  //# sourceMappingURL=index.js.map