@nxgt/janus-drizzle 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +378 -303
  3. package/dist/index.js.map +20 -6
  4. package/dist/relations/context.d.ts +9 -0
  5. package/dist/relations/context.d.ts.map +1 -0
  6. package/dist/{relations.d.ts → relations/index.d.ts} +3 -3
  7. package/dist/relations/index.d.ts.map +1 -0
  8. package/dist/relations/reads.d.ts +9 -0
  9. package/dist/relations/reads.d.ts.map +1 -0
  10. package/dist/relations/tuples.d.ts +12 -0
  11. package/dist/relations/tuples.d.ts.map +1 -0
  12. package/dist/relations/writes.d.ts +7 -0
  13. package/dist/relations/writes.d.ts.map +1 -0
  14. package/dist/stores/identity-tables.d.ts +5 -0
  15. package/dist/stores/identity-tables.d.ts.map +1 -0
  16. package/dist/{stores.d.ts → stores/index.d.ts} +4 -6
  17. package/dist/stores/index.d.ts.map +1 -0
  18. package/dist/stores/logins.d.ts +13 -0
  19. package/dist/stores/logins.d.ts.map +1 -0
  20. package/dist/stores/records.d.ts +23 -0
  21. package/dist/stores/records.d.ts.map +1 -0
  22. package/dist/stores/sessions.d.ts +5 -0
  23. package/dist/stores/sessions.d.ts.map +1 -0
  24. package/dist/stores/tokens.d.ts +5 -0
  25. package/dist/stores/tokens.d.ts.map +1 -0
  26. package/dist/stores/users.d.ts +5 -0
  27. package/dist/stores/users.d.ts.map +1 -0
  28. package/dist/tables/columns.d.ts +22 -0
  29. package/dist/tables/columns.d.ts.map +1 -0
  30. package/dist/{tables.d.ts → tables/index.d.ts} +16 -1
  31. package/dist/tables/index.d.ts.map +1 -0
  32. package/dist/tables/logins.d.ts +74 -0
  33. package/dist/tables/logins.d.ts.map +1 -0
  34. package/dist/tables/relations.d.ts +133 -0
  35. package/dist/tables/relations.d.ts.map +1 -0
  36. package/dist/tables/sessions.d.ts +131 -0
  37. package/dist/tables/sessions.d.ts.map +1 -0
  38. package/dist/tables/tokens.d.ts +153 -0
  39. package/dist/tables/tokens.d.ts.map +1 -0
  40. package/dist/tables/users.d.ts +276 -0
  41. package/dist/tables/users.d.ts.map +1 -0
  42. package/docs/guide/migrations.md +35 -4
  43. package/docs/roadmap.md +6 -0
  44. package/docs/troubleshooting.md +21 -0
  45. package/package.json +3 -3
  46. package/dist/relations.d.ts.map +0 -1
  47. package/dist/stores.d.ts.map +0 -1
  48. 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,58 @@ 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
+ secondFactorRecoveryCodes: text2("second_factor_recovery_codes").array(),
114
+ emailVerifiedAt: at("email_verified_at"),
115
+ version: integer2("version").notNull(),
116
+ createdAt: at("created_at").notNull(),
117
+ updatedAt: at("updated_at").notNull()
105
118
  }, (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)
119
+ unique3("users_id_type_unique").on(t.id, t.type),
120
+ index5("users_type_id").on(t.type, t.id),
121
+ check2("users_password_whole", sql2`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`),
122
+ 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 and ${t.secondFactorRecoveryCodes} is null))`),
123
+ check2("users_second_factor_values", sql2`${t.secondFactorMethod} in ('totp') and ${t.secondFactorLastStep} >= 0`)
108
124
  ]);
125
+ }
126
+
127
+ // src/tables/index.ts
128
+ function defineJanusTables(options = {}) {
129
+ const { schema } = options;
130
+ const table = schema === undefined ? pgTable : schema.table;
131
+ const users = usersTable(table);
132
+ const logins = loginsTable(table, users);
133
+ const sessions = sessionsTable(table);
134
+ const tokens = tokensTable(table);
135
+ const relations = relationsTable(table);
109
136
  return { users, logins, sessions, tokens, relations };
110
137
  }
111
138
 
@@ -129,62 +156,12 @@ function loginTaken(operation, type, login) {
129
156
  return new StoreConflict("login", `${operation}: the login is taken by another ${type}`, { login, userType: type, operation });
130
157
  }
131
158
 
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
- }
159
+ // src/relations/reads.ts
160
+ import { and as and2, asc, eq as eq2, gt, isNotNull, isNull as isNull2 } from "drizzle-orm";
161
+
162
+ // src/relations/tuples.ts
163
+ import { isSubjectSet } from "@nxgt/janus";
164
+ import { and, eq, isNull } from "drizzle-orm";
188
165
  function inOrder(tuples) {
189
166
  const keyOf = (tuple) => JSON.stringify([
190
167
  tuple.object.type,
@@ -199,194 +176,103 @@ function inOrder(tuples) {
199
176
  return x < y ? -1 : x > y ? 1 : 0;
200
177
  });
201
178
  }
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();
179
+ function rowOf(tuple) {
228
180
  return {
229
- users: userStore(db, tables),
230
- sessions: sessionStore(db, tables),
231
- tokens: tokenStore(db, tables)
181
+ objectType: tuple.object.type,
182
+ objectId: tuple.object.id,
183
+ relation: tuple.relation,
184
+ subjectType: tuple.subject.type,
185
+ subjectId: tuple.subject.id,
186
+ subjectRelation: isSubjectSet(tuple.subject) ? tuple.subject.relation : null
232
187
  };
233
188
  }
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
- };
189
+ function subjectIs(relations, subject) {
190
+ return and(eq(relations.subjectType, subject.type), eq(relations.subjectId, subject.id), isSubjectSet(subject) ? eq(relations.subjectRelation, subject.relation) : isNull(relations.subjectRelation));
315
191
  }
316
- function sessionStore(db, tables) {
317
- const run$ = (operation, body) => run("sessions", operation, body);
192
+ function matching(relations, tuple) {
193
+ return and(eq(relations.objectType, tuple.object.type), eq(relations.objectId, tuple.object.id), eq(relations.relation, tuple.relation), subjectIs(relations, tuple.subject));
194
+ }
195
+
196
+ // src/relations/reads.ts
197
+ async function has({ db, relations }, tuple) {
198
+ const found = await db.select({ relation: relations.relation }).from(relations).where(matching(relations, tuple)).limit(1);
199
+ return found.length === 1;
200
+ }
201
+ async function findSubjectSets(context, object, relation) {
202
+ return (await holding(context, object, relation, true)).map((row) => ({
203
+ type: row.type,
204
+ id: row.id,
205
+ relation: row.relation ?? ""
206
+ }));
207
+ }
208
+ async function findEntities(context, object, relation) {
209
+ return (await holding(context, object, relation, false)).map((row) => ({ type: row.type, id: row.id }));
210
+ }
211
+ function holding({ db, relations }, object, relation, sets) {
212
+ return db.select({
213
+ type: relations.subjectType,
214
+ id: relations.subjectId,
215
+ relation: relations.subjectRelation
216
+ }).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)));
217
+ }
218
+ async function findObjects({ db, relations }, { type, relation, subject, after, limit }) {
219
+ const conditions = [
220
+ subjectIs(relations, subject),
221
+ eq2(relations.relation, relation),
222
+ eq2(relations.objectType, type)
223
+ ];
224
+ if (after !== null)
225
+ conditions.push(gt(relations.objectId, after));
226
+ const found = await db.select({ id: relations.objectId }).from(relations).where(and2(...conditions)).orderBy(asc(relations.objectId)).limit(limit + 1);
227
+ const items = found.slice(0, limit).map((row) => row.id);
228
+ const last = items.at(-1);
318
229
  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
- })
230
+ items,
231
+ nextCursor: found.length > limit && last !== undefined ? last : null
349
232
  };
350
233
  }
351
- function tokenStore(db, tables) {
352
- const run$ = (operation, body) => run("tokens", operation, body);
234
+
235
+ // src/relations/writes.ts
236
+ import { withTransaction } from "@nxgt/drizzle/pg";
237
+ import { and as and3, eq as eq3, or } from "drizzle-orm";
238
+ async function write({ db, relations }, add, remove) {
239
+ if (add.length + remove.length <= 1)
240
+ return apply(db, relations, add, remove);
241
+ await withTransaction(db, (tx) => apply(tx, relations, add, remove));
242
+ }
243
+ async function apply(tx, relations, add, remove) {
244
+ for (const tuple of inOrder(remove)) {
245
+ await tx.delete(relations).where(matching(relations, tuple));
246
+ }
247
+ if (add.length > 0) {
248
+ await tx.insert(relations).values(inOrder(add).map(rowOf)).onConflictDoNothing();
249
+ }
250
+ }
251
+ async function deleteEntity({ db, relations }, entity) {
252
+ 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 });
253
+ return deleted.length;
254
+ }
255
+
256
+ // src/relations/index.ts
257
+ function createDrizzleRelations(db, options = {}) {
258
+ const tables = options.tables ?? defineJanusTables();
259
+ const context = { db, relations: tables.relations };
260
+ const run$ = (operation, body) => run("relations", operation, body);
353
261
  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
- })
262
+ write: ({ add = [], remove = [] }) => run$("write", () => write(context, add, remove)),
263
+ has: (tuple) => run$("has", () => has(context, tuple)),
264
+ findSubjectSets: (object, relation) => run$("findSubjectSets", () => findSubjectSets(context, object, relation)),
265
+ findEntities: (object, relation) => run$("findEntities", () => findEntities(context, object, relation)),
266
+ findObjects: ({ type, relation, subject, after, limit }) => run$("findObjects", () => findObjects(context, { type, relation, subject, after, limit })),
267
+ deleteEntity: (entity) => run$("deleteEntity", () => deleteEntity(context, entity))
388
268
  };
389
269
  }
270
+
271
+ // src/stores/sessions.ts
272
+ import { and as and4, eq as eq4, isNull as isNull3, lte, ne, sql as sql4 } from "drizzle-orm";
273
+
274
+ // src/stores/records.ts
275
+ import { sql as sql3 } from "drizzle-orm";
390
276
  function toUserRow(record) {
391
277
  return {
392
278
  id: record.id,
@@ -428,11 +314,13 @@ function toUserSet(patch) {
428
314
  return set;
429
315
  }
430
316
  function toSecondFactorColumns(secondFactor) {
317
+ const codes = secondFactor?.recoveryCodes ?? [];
431
318
  return {
432
319
  secondFactorMethod: secondFactor?.method ?? null,
433
320
  secondFactorSecret: secondFactor?.secret ?? null,
434
321
  secondFactorConfirmedAt: secondFactor?.confirmedAt ?? null,
435
- secondFactorLastStep: secondFactor?.lastStep ?? null
322
+ secondFactorLastStep: secondFactor?.lastStep ?? null,
323
+ secondFactorRecoveryCodes: codes.length === 0 ? null : [...codes]
436
324
  };
437
325
  }
438
326
  function toUser(row) {
@@ -448,7 +336,8 @@ function toUser(row) {
448
336
  method: row.secondFactorMethod,
449
337
  secret: row.secondFactorSecret,
450
338
  confirmedAt: row.secondFactorConfirmedAt,
451
- lastStep: row.secondFactorLastStep
339
+ lastStep: row.secondFactorLastStep,
340
+ recoveryCodes: [...row.secondFactorRecoveryCodes ?? []]
452
341
  },
453
342
  emailVerifiedAt: row.emailVerifiedAt,
454
343
  version: row.version,
@@ -468,7 +357,193 @@ function toSession(row) {
468
357
  };
469
358
  }
470
359
  function stamp(at) {
471
- return sql2`${at.toISOString()}::timestamptz`;
360
+ return sql3`${at.toISOString()}::timestamptz`;
361
+ }
362
+
363
+ // src/stores/sessions.ts
364
+ function sessionStore(db, tables) {
365
+ const run$ = (operation, body) => run("sessions", operation, body);
366
+ return {
367
+ insertSession: (record) => run$("insertSession", async () => {
368
+ await db.insert(tables.sessions).values(record).onConflictDoNothing({ target: tables.sessions.id });
369
+ }),
370
+ findSessionByTokenHash: (tokenHash) => run$("findSessionByTokenHash", async () => {
371
+ const [row] = await db.select().from(tables.sessions).where(eq4(tables.sessions.tokenHash, tokenHash));
372
+ return row === undefined ? null : toSession(row);
373
+ }),
374
+ extendSession: (id, expiresAt) => run$("extendSession", async () => {
375
+ const [row] = await db.update(tables.sessions).set({ expiresAt }).where(and4(eq4(tables.sessions.id, id), isNull3(tables.sessions.revokedAt))).returning();
376
+ return row === undefined ? null : toSession(row);
377
+ }),
378
+ revokeSession: (id, at) => run$("revokeSession", () => revokeSession(db, tables, id, at)),
379
+ revokeUserSessions: (userId, at, except) => run$("revokeUserSessions", () => revokeUserSessions(db, tables, userId, at, except)),
380
+ deleteUserSessions: (userId) => run$("deleteUserSessions", async () => {
381
+ const deleted = await db.delete(tables.sessions).where(eq4(tables.sessions.userId, userId)).returning({ id: tables.sessions.id });
382
+ return deleted.length;
383
+ }),
384
+ deleteExpiredSessions: (before) => run$("deleteExpiredSessions", async () => {
385
+ const deleted = await db.delete(tables.sessions).where(lte(tables.sessions.expiresAt, before)).returning({ id: tables.sessions.id });
386
+ return deleted.length;
387
+ })
388
+ };
389
+ }
390
+ async function revokeSession(db, tables, id, at) {
391
+ const matched = await db.update(tables.sessions).set({
392
+ revokedAt: sql4`coalesce(${tables.sessions.revokedAt}, ${stamp(at)})`
393
+ }).where(eq4(tables.sessions.id, id)).returning({ id: tables.sessions.id });
394
+ return matched.length === 1;
395
+ }
396
+ async function revokeUserSessions(db, tables, userId, at, except) {
397
+ const standing = and4(eq4(tables.sessions.userId, userId), isNull3(tables.sessions.revokedAt));
398
+ 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 });
399
+ return revoked.length;
400
+ }
401
+
402
+ // src/stores/tokens.ts
403
+ import { and as and5, eq as eq5, isNull as isNull4, ne as ne2, sql as sql5 } from "drizzle-orm";
404
+ function tokenStore(db, tables) {
405
+ const run$ = (operation, body) => run("tokens", operation, body);
406
+ return {
407
+ insertToken: (record) => run$("insertToken", async () => {
408
+ await db.insert(tables.tokens).values(record).onConflictDoNothing();
409
+ }),
410
+ consumeToken: (tokenHash, kind, at) => run$("consumeToken", () => consumeToken(db, tables, tokenHash, kind, at)),
411
+ countAttempt: (tokenHash, kind) => run$("countAttempt", () => countAttempt(db, tables, tokenHash, kind)),
412
+ spendUserTokens: (userId, kind, at, except) => run$("spendUserTokens", async () => {
413
+ 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 });
414
+ return spent.length;
415
+ }),
416
+ deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
417
+ const deleted = await db.delete(tables.tokens).where(eq5(tables.tokens.userId, userId)).returning({ tokenHash: tables.tokens.tokenHash });
418
+ return deleted.length;
419
+ })
420
+ };
421
+ }
422
+ async function consumeToken(db, tables, tokenHash, kind, at) {
423
+ 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"));
424
+ const [spent] = await db.with(before).update(tables.tokens).set({
425
+ spentAt: sql5`coalesce(${tables.tokens.spentAt}, ${stamp(at)})`
426
+ }).from(before).where(eq5(tables.tokens.tokenHash, before.tokenHash)).returning({
427
+ tokenHash: before.tokenHash,
428
+ kind: before.kind,
429
+ userId: before.userId,
430
+ address: before.address,
431
+ codeHash: before.codeHash,
432
+ attempts: before.attempts,
433
+ expiresAt: before.expiresAt,
434
+ spentAt: before.spentAt,
435
+ createdAt: before.createdAt
436
+ });
437
+ return spent === undefined ? null : { ...spent, userId: spent.userId };
438
+ }
439
+ async function countAttempt(db, tables, tokenHash, kind) {
440
+ const match = and5(eq5(tables.tokens.tokenHash, tokenHash), eq5(tables.tokens.kind, kind));
441
+ const [counted] = await db.update(tables.tokens).set({ attempts: sql5`${tables.tokens.attempts} + 1` }).where(and5(match, isNull4(tables.tokens.spentAt))).returning();
442
+ const row = counted ?? (await db.select().from(tables.tokens).where(match))[0];
443
+ return row === undefined ? null : { ...row, userId: row.userId };
444
+ }
445
+
446
+ // src/stores/users.ts
447
+ import { withTransaction as withTransaction2 } from "@nxgt/drizzle/pg";
448
+ import { NotFoundError, StoreConflict as StoreConflict2 } from "@nxgt/janus";
449
+ import { and as and6, asc as asc2, eq as eq6, gt as gt2, sql as sql6 } from "drizzle-orm";
450
+
451
+ // src/stores/logins.ts
452
+ async function claimLogins(tx, tables, operation, user, logins) {
453
+ const distinct = [...new Set(logins)].sort();
454
+ if (distinct.length === 0)
455
+ return;
456
+ const written = await tx.insert(tables.logins).values(distinct.map((login) => ({ type: user.type, login, userId: user.id }))).onConflictDoNothing().returning({ login: tables.logins.login });
457
+ if (written.length === distinct.length)
458
+ return;
459
+ const claimed = new Set(written.map((row) => row.login));
460
+ const taken = distinct.find((login) => !claimed.has(login)) ?? "";
461
+ throw loginTaken(operation, user.type, taken);
462
+ }
463
+
464
+ // src/stores/users.ts
465
+ function userStore(db, tables) {
466
+ const run$ = (operation, body) => run("users", operation, body);
467
+ return {
468
+ insertUser: (record) => run$("insertUser", () => insertUser(db, tables, record)),
469
+ findUser: (id) => run$("findUser", () => findUser(db, tables, id)),
470
+ findUserByLogin: (type, login) => run$("findUserByLogin", async () => {
471
+ 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)));
472
+ return row === undefined ? null : toUser(row.user);
473
+ }),
474
+ listUsers: ({ type, after, limit }) => run$("listUsers", async () => {
475
+ 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);
476
+ const items = found.slice(0, limit).map(toUser);
477
+ const last = items.at(-1);
478
+ return {
479
+ items,
480
+ nextCursor: found.length > limit && last !== undefined ? last.id : null
481
+ };
482
+ }),
483
+ updateUser: (id, patch, ifVersion) => run$("updateUser", () => withTransaction2(db, (tx) => updateUser(tx, tables, id, patch, ifVersion))),
484
+ deleteUser: (id) => run$("deleteUser", async () => {
485
+ const deleted = await db.delete(tables.users).where(eq6(tables.users.id, id)).returning({ id: tables.users.id });
486
+ return deleted.length === 1;
487
+ })
488
+ };
489
+ }
490
+ async function findUser(db, tables, id) {
491
+ const [row] = await db.select().from(tables.users).where(eq6(tables.users.id, id));
492
+ return row === undefined ? null : toUser(row);
493
+ }
494
+ async function insertUser(db, tables, record) {
495
+ const inserted = await withTransaction2(db, async (tx) => {
496
+ const [row] = await tx.insert(tables.users).values(toUserRow(record)).onConflictDoNothing({ target: tables.users.id }).returning();
497
+ if (row === undefined)
498
+ return null;
499
+ await claimLogins(tx, tables, "insertUser", record, record.logins);
500
+ return toUser(row);
501
+ });
502
+ if (inserted !== null)
503
+ return inserted;
504
+ const stored = await findUser(db, tables, record.id);
505
+ if (stored !== null)
506
+ return stored;
507
+ throw new NotFoundError("insertUser: the user was deleted meanwhile", {
508
+ userId: record.id,
509
+ operation: "insertUser"
510
+ });
511
+ }
512
+ async function updateUser(tx, tables, id, patch, ifVersion) {
513
+ const [row] = await tx.update(tables.users).set({
514
+ ...toUserSet(patch),
515
+ version: sql6`${tables.users.version} + 1`
516
+ }).where(and6(eq6(tables.users.id, id), eq6(tables.users.version, ifVersion))).returning();
517
+ if (row === undefined) {
518
+ const [stored] = await tx.select({ version: tables.users.version }).from(tables.users).where(eq6(tables.users.id, id));
519
+ if (stored === undefined) {
520
+ throw new NotFoundError("updateUser: no user has this id", {
521
+ userId: id,
522
+ operation: "updateUser"
523
+ });
524
+ }
525
+ throw new StoreConflict2("version", `updateUser: expected version ${ifVersion}, found ${stored.version}`, {
526
+ userId: id,
527
+ expectedVersion: ifVersion,
528
+ actualVersion: stored.version,
529
+ operation: "updateUser"
530
+ });
531
+ }
532
+ if (patch.logins !== undefined) {
533
+ await tx.delete(tables.logins).where(eq6(tables.logins.userId, id));
534
+ await claimLogins(tx, tables, "updateUser", row, patch.logins);
535
+ }
536
+ return toUser(row);
537
+ }
538
+
539
+ // src/stores/index.ts
540
+ function createDrizzleStores(db, options = {}) {
541
+ const tables = options.tables ?? defineJanusTables();
542
+ return {
543
+ users: userStore(db, tables),
544
+ sessions: sessionStore(db, tables),
545
+ tokens: tokenStore(db, tables)
546
+ };
472
547
  }
473
548
 
474
549
  // src/adapter.ts
@@ -485,5 +560,5 @@ export {
485
560
  defineJanusTables
486
561
  };
487
562
 
488
- //# debugId=7AD52CBFCEA4D74A64756E2164756E21
563
+ //# debugId=BBBB8A4136A5079264756E2164756E21
489
564
  //# sourceMappingURL=index.js.map