@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.
- package/README.md +9 -1
- package/dist/index.js +378 -303
- package/dist/index.js.map +20 -6
- package/dist/relations/context.d.ts +9 -0
- package/dist/relations/context.d.ts.map +1 -0
- package/dist/{relations.d.ts → relations/index.d.ts} +3 -3
- package/dist/relations/index.d.ts.map +1 -0
- package/dist/relations/reads.d.ts +9 -0
- package/dist/relations/reads.d.ts.map +1 -0
- package/dist/relations/tuples.d.ts +12 -0
- package/dist/relations/tuples.d.ts.map +1 -0
- package/dist/relations/writes.d.ts +7 -0
- package/dist/relations/writes.d.ts.map +1 -0
- package/dist/stores/identity-tables.d.ts +5 -0
- package/dist/stores/identity-tables.d.ts.map +1 -0
- package/dist/{stores.d.ts → stores/index.d.ts} +4 -6
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/logins.d.ts +13 -0
- package/dist/stores/logins.d.ts.map +1 -0
- package/dist/stores/records.d.ts +23 -0
- package/dist/stores/records.d.ts.map +1 -0
- package/dist/stores/sessions.d.ts +5 -0
- package/dist/stores/sessions.d.ts.map +1 -0
- package/dist/stores/tokens.d.ts +5 -0
- package/dist/stores/tokens.d.ts.map +1 -0
- package/dist/stores/users.d.ts +5 -0
- package/dist/stores/users.d.ts.map +1 -0
- package/dist/tables/columns.d.ts +22 -0
- package/dist/tables/columns.d.ts.map +1 -0
- package/dist/{tables.d.ts → tables/index.d.ts} +16 -1
- package/dist/tables/index.d.ts.map +1 -0
- package/dist/tables/logins.d.ts +74 -0
- package/dist/tables/logins.d.ts.map +1 -0
- package/dist/tables/relations.d.ts +133 -0
- package/dist/tables/relations.d.ts.map +1 -0
- package/dist/tables/sessions.d.ts +131 -0
- package/dist/tables/sessions.d.ts.map +1 -0
- package/dist/tables/tokens.d.ts +153 -0
- package/dist/tables/tokens.d.ts.map +1 -0
- package/dist/tables/users.d.ts +276 -0
- package/dist/tables/users.d.ts.map +1 -0
- package/docs/guide/migrations.md +35 -4
- package/docs/roadmap.md +6 -0
- package/docs/troubleshooting.md +21 -0
- package/package.json +3 -3
- package/dist/relations.d.ts.map +0 -1
- package/dist/stores.d.ts.map +0 -1
- package/dist/tables.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,58 +1,19 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
index4("tokens_user_id").on(t.userId)
|
|
97
85
|
]);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
235
|
-
|
|
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
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
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=
|
|
563
|
+
//# debugId=BBBB8A4136A5079264756E2164756E21
|
|
489
564
|
//# sourceMappingURL=index.js.map
|