better-auth-instantdb 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/adapter/create-schema.d.ts +9 -0
- package/dist/adapter/create-schema.js +178 -0
- package/dist/adapter/instant-adapter.d.ts +25 -0
- package/dist/adapter/instant-adapter.js +273 -0
- package/dist/client-plugin.d.ts +2143 -0
- package/dist/client-plugin.js +21 -0
- package/dist/create-schema.d.ts +25 -0
- package/dist/create-schema.js +115 -0
- package/dist/create-schema.js.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +160 -0
- package/dist/instant-adapter.d.ts +26 -0
- package/dist/instant-adapter.js +214 -0
- package/dist/instant-adapter.js.map +1 -0
- package/dist/instant-auth.d.ts +3 -0
- package/dist/instant-auth.js +9 -0
- package/dist/lib/instant-auth.d.ts +3 -0
- package/dist/lib/instant-auth.js +9 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +22 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/react/client-plugin.d.ts +2143 -0
- package/dist/react/client-plugin.js +21 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +4 -0
- package/dist/react/instant-auth.d.ts +7 -0
- package/dist/react/instant-auth.js +5 -0
- package/dist/react/react.d.ts +2 -0
- package/dist/react/react.js +2 -0
- package/dist/react/types.d.ts +6 -0
- package/dist/react/types.js +1 -0
- package/dist/react/use-hydrated.d.ts +1 -0
- package/dist/react/use-hydrated.js +7 -0
- package/dist/react/use-instant-auth.d.ts +8 -0
- package/dist/react/use-instant-auth.js +13 -0
- package/dist/react/use-instant-session.d.ts +32 -0
- package/dist/react/use-instant-session.js +25 -0
- package/dist/react/use-persistent-session.d.ts +27 -0
- package/dist/react/use-persistent-session.js +49 -0
- package/dist/react/use-session.d.ts +0 -0
- package/dist/react/use-session.js +1 -0
- package/dist/react/with-instant.d.ts +3 -0
- package/dist/react/with-instant.js +47 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/shared/instant-auth.d.ts +4 -0
- package/dist/shared/instant-auth.js +9 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +9 -0
- package/package.json +70 -0
- package/src/adapter/create-schema.ts +232 -0
- package/src/adapter/instant-adapter.ts +422 -0
- package/src/index.ts +2 -0
- package/src/lib/utils.ts +24 -0
- package/src/react/index.ts +4 -0
- package/src/react/instant-auth.tsx +17 -0
- package/src/react/types.ts +9 -0
- package/src/react/use-hydrated.ts +13 -0
- package/src/react/use-instant-auth.ts +28 -0
- package/src/react/use-instant-session.ts +46 -0
- package/src/react/use-persistent-session.ts +64 -0
- package/src/shared/instant-auth.ts +18 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import type { BetterAuthDBSchema } from "@better-auth/core/db"
|
|
2
|
+
import {
|
|
3
|
+
type InstantAdminDatabase,
|
|
4
|
+
type InstaQLParams,
|
|
5
|
+
id
|
|
6
|
+
} from "@instantdb/admin"
|
|
7
|
+
import {
|
|
8
|
+
createAdapterFactory,
|
|
9
|
+
type DBAdapterDebugLogOption,
|
|
10
|
+
type Where
|
|
11
|
+
} from "better-auth/adapters"
|
|
12
|
+
|
|
13
|
+
import { fieldNameToLabel, prettyObject } from "../lib/utils"
|
|
14
|
+
import { createSchema } from "./create-schema"
|
|
15
|
+
|
|
16
|
+
type Direction = "asc" | "desc"
|
|
17
|
+
type Order = { [key: string]: Direction }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the InstantDB entity name for a given model name
|
|
21
|
+
*/
|
|
22
|
+
function getEntityName(
|
|
23
|
+
modelName: string,
|
|
24
|
+
tableKey: string,
|
|
25
|
+
usePlural: boolean
|
|
26
|
+
): string {
|
|
27
|
+
if (modelName === "user") {
|
|
28
|
+
return "$users"
|
|
29
|
+
}
|
|
30
|
+
return usePlural ? `${tableKey}s` : tableKey
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds entity name mapping from schema
|
|
35
|
+
*/
|
|
36
|
+
function buildEntityNameMap(
|
|
37
|
+
schema: BetterAuthDBSchema,
|
|
38
|
+
usePlural: boolean
|
|
39
|
+
): Record<string, string> {
|
|
40
|
+
const entityNameMap: Record<string, string> = {}
|
|
41
|
+
for (const [key, table] of Object.entries(schema)) {
|
|
42
|
+
const { modelName } = table
|
|
43
|
+
entityNameMap[modelName] = getEntityName(modelName, key, usePlural)
|
|
44
|
+
}
|
|
45
|
+
return entityNameMap
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates link transactions for fields with references
|
|
50
|
+
*/
|
|
51
|
+
function createLinkTransactions({
|
|
52
|
+
db,
|
|
53
|
+
model,
|
|
54
|
+
modelSchema,
|
|
55
|
+
data,
|
|
56
|
+
entityNameMap
|
|
57
|
+
}: {
|
|
58
|
+
db: InstantAdminDatabase<any, any>
|
|
59
|
+
model: string
|
|
60
|
+
modelSchema: BetterAuthDBSchema[string]
|
|
61
|
+
data: Record<string, unknown>
|
|
62
|
+
entityNameMap: Record<string, string>
|
|
63
|
+
}): any[] {
|
|
64
|
+
const linkTransactions: any[] = []
|
|
65
|
+
const { fields, modelName } = modelSchema
|
|
66
|
+
|
|
67
|
+
for (const [fieldKey, field] of Object.entries(fields)) {
|
|
68
|
+
const { references } = field
|
|
69
|
+
|
|
70
|
+
if (references) {
|
|
71
|
+
const { model: targetModel } = references
|
|
72
|
+
const targetEntityName = entityNameMap[targetModel]
|
|
73
|
+
|
|
74
|
+
if (!targetEntityName) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`Warning: Could not find entity name for model "${targetModel}" referenced by ${modelName}.${fieldKey}`
|
|
77
|
+
)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if data has a value for this reference field
|
|
82
|
+
const fieldValue = data[fieldKey]
|
|
83
|
+
if (fieldValue != null) {
|
|
84
|
+
// Generate forward label from field name, using target model if field doesn't end with "id"
|
|
85
|
+
const forwardLabel = fieldNameToLabel(fieldKey, targetModel)
|
|
86
|
+
|
|
87
|
+
// Create link transaction
|
|
88
|
+
const linkParams: Record<string, string | string[]> = {
|
|
89
|
+
[forwardLabel]: fieldValue as string | string[]
|
|
90
|
+
}
|
|
91
|
+
const linkTransaction = db.tx[model][data.id as string].link(linkParams)
|
|
92
|
+
|
|
93
|
+
linkTransactions.push(linkTransaction)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return linkTransactions
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The InstantDB adapter config options.
|
|
103
|
+
*/
|
|
104
|
+
interface InstantAdapterConfig {
|
|
105
|
+
/**
|
|
106
|
+
* The InstantDB admin database instance.
|
|
107
|
+
*/
|
|
108
|
+
db: InstantAdminDatabase<any, any>
|
|
109
|
+
/**
|
|
110
|
+
* If the table names in the schema are plural.
|
|
111
|
+
*/
|
|
112
|
+
usePlural?: boolean
|
|
113
|
+
/**
|
|
114
|
+
* Helps you debug issues with the adapter.
|
|
115
|
+
*/
|
|
116
|
+
debugLogs?: DBAdapterDebugLogOption
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The InstantDB adapter.
|
|
121
|
+
*/
|
|
122
|
+
export const instantAdapter = ({
|
|
123
|
+
db,
|
|
124
|
+
usePlural = true,
|
|
125
|
+
debugLogs = false
|
|
126
|
+
}: InstantAdapterConfig) => {
|
|
127
|
+
return createAdapterFactory({
|
|
128
|
+
config: {
|
|
129
|
+
customIdGenerator: id,
|
|
130
|
+
adapterId: "instantdb-adapter", // A unique identifier for the adapter.
|
|
131
|
+
adapterName: "InstantDB Adapter", // The name of the adapter.
|
|
132
|
+
usePlural, // Whether the table names in the schema are plural.
|
|
133
|
+
debugLogs, // Whether to enable debug logs.
|
|
134
|
+
supportsJSON: true, // Whether the database supports JSON. (Default: false)
|
|
135
|
+
supportsDates: false, // Whether the database supports dates. (Default: true)
|
|
136
|
+
supportsBooleans: true, // Whether the database supports booleans. (Default: true)
|
|
137
|
+
supportsNumericIds: false // Whether the database supports auto-incrementing numeric IDs. (Default: true)
|
|
138
|
+
},
|
|
139
|
+
adapter: ({ debugLog, getDefaultModelName, getFieldName, schema }) => {
|
|
140
|
+
return {
|
|
141
|
+
create: async ({ data, model }) => {
|
|
142
|
+
const defaultModelName = getDefaultModelName(model)
|
|
143
|
+
const modelSchema = schema[defaultModelName]
|
|
144
|
+
|
|
145
|
+
// Create the InstantDB token and override session.token
|
|
146
|
+
if (defaultModelName === "session") {
|
|
147
|
+
// Get the $users entity for this session's userId
|
|
148
|
+
const result = await db.query({
|
|
149
|
+
$users: { $: { where: { id: data.userId } } }
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const $users = result.$users
|
|
153
|
+
|
|
154
|
+
if (!$users.length) {
|
|
155
|
+
throw new Error(`$users entity not found: ${data.userId}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const $user = $users[0]
|
|
159
|
+
|
|
160
|
+
// Create the InstantDB token and override session.token
|
|
161
|
+
|
|
162
|
+
debugLog("Create Token", $user.email)
|
|
163
|
+
|
|
164
|
+
const token = await db.auth.createToken($user.email as string)
|
|
165
|
+
const tokenField = getFieldName({ model, field: "token" })
|
|
166
|
+
|
|
167
|
+
Object.assign(data, { [tokenField]: token })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (defaultModelName === "user") {
|
|
171
|
+
model = "$users"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
debugLog("Create", model, prettyObject(data))
|
|
175
|
+
|
|
176
|
+
// Build entity name map for link resolution
|
|
177
|
+
const entityNameMap = buildEntityNameMap(schema, usePlural)
|
|
178
|
+
|
|
179
|
+
// Create the main entity transaction
|
|
180
|
+
const createTransaction = db.tx[model][data.id].create(data)
|
|
181
|
+
|
|
182
|
+
// Create link transactions for fields with references
|
|
183
|
+
const linkTransactions = createLinkTransactions({
|
|
184
|
+
db,
|
|
185
|
+
model,
|
|
186
|
+
modelSchema,
|
|
187
|
+
data,
|
|
188
|
+
entityNameMap
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Combine all transactions and execute in a single transaction
|
|
192
|
+
const allTransactions = [createTransaction, ...linkTransactions]
|
|
193
|
+
await db.transact(allTransactions)
|
|
194
|
+
|
|
195
|
+
return data
|
|
196
|
+
},
|
|
197
|
+
update: async ({ update, model, where }) => {
|
|
198
|
+
if (getDefaultModelName(model) === "user") {
|
|
199
|
+
model = "$users"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
203
|
+
|
|
204
|
+
if (!entities.length) return null
|
|
205
|
+
|
|
206
|
+
debugLog(
|
|
207
|
+
"Update:",
|
|
208
|
+
entities.map((entity) => entity.id),
|
|
209
|
+
prettyObject(update)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const transactions = entities.map((entity) =>
|
|
213
|
+
db.tx[model][entity.id].update(update as Record<string, unknown>)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
await db.transact(transactions)
|
|
217
|
+
|
|
218
|
+
return { ...entities[0], ...update }
|
|
219
|
+
},
|
|
220
|
+
updateMany: async ({ update, model, where }) => {
|
|
221
|
+
if (getDefaultModelName(model) === "user") {
|
|
222
|
+
model = "$users"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
226
|
+
|
|
227
|
+
if (!entities.length) return 0
|
|
228
|
+
|
|
229
|
+
debugLog(
|
|
230
|
+
"Update:",
|
|
231
|
+
entities.map((entity) => entity.id),
|
|
232
|
+
prettyObject(update)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const transactions = entities.map((entity) =>
|
|
236
|
+
db.tx[model][entity.id].update(update)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
await db.transact(transactions)
|
|
240
|
+
|
|
241
|
+
return entities.length
|
|
242
|
+
},
|
|
243
|
+
delete: async ({ model, where }) => {
|
|
244
|
+
if (getDefaultModelName(model) === "user") {
|
|
245
|
+
model = "$users"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
249
|
+
|
|
250
|
+
if (!entities.length) return
|
|
251
|
+
|
|
252
|
+
const transactions = entities.map((entity) =>
|
|
253
|
+
db.tx[model][entity.id].delete()
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
await db.transact(transactions)
|
|
257
|
+
|
|
258
|
+
if (getDefaultModelName(model) === "session") {
|
|
259
|
+
Promise.all(
|
|
260
|
+
entities.map(async (entity) => {
|
|
261
|
+
try {
|
|
262
|
+
const tokenField = getFieldName({ model, field: "token" })
|
|
263
|
+
await db.auth.signOut({
|
|
264
|
+
refresh_token: entity[tokenField]
|
|
265
|
+
})
|
|
266
|
+
} catch {}
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
deleteMany: async ({ model, where }) => {
|
|
272
|
+
if (getDefaultModelName(model) === "user") {
|
|
273
|
+
model = "$users"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
277
|
+
|
|
278
|
+
if (!entities.length) return 0
|
|
279
|
+
|
|
280
|
+
const transactions = entities.map((entity) =>
|
|
281
|
+
db.tx[model][entity.id].delete()
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
await db.transact(transactions)
|
|
285
|
+
|
|
286
|
+
if (getDefaultModelName(model) === "session") {
|
|
287
|
+
Promise.all(
|
|
288
|
+
entities.map(async (entity) => {
|
|
289
|
+
try {
|
|
290
|
+
const tokenField = getFieldName({ model, field: "token" })
|
|
291
|
+
await db.auth.signOut({
|
|
292
|
+
refresh_token: entity[tokenField]
|
|
293
|
+
})
|
|
294
|
+
} catch {}
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return entities.length
|
|
300
|
+
},
|
|
301
|
+
findOne: async ({ model, where }) => {
|
|
302
|
+
if (getDefaultModelName(model) === "user") {
|
|
303
|
+
model = "$users"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
307
|
+
|
|
308
|
+
if (entities.length) return entities[0]
|
|
309
|
+
|
|
310
|
+
return null
|
|
311
|
+
},
|
|
312
|
+
findMany: async ({ model, where, limit, sortBy, offset }) => {
|
|
313
|
+
if (getDefaultModelName(model) === "user") {
|
|
314
|
+
model = "$users"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const entities = await fetchEntities({
|
|
318
|
+
db,
|
|
319
|
+
model,
|
|
320
|
+
where,
|
|
321
|
+
limit,
|
|
322
|
+
sortBy,
|
|
323
|
+
offset,
|
|
324
|
+
debugLog
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
return entities
|
|
328
|
+
},
|
|
329
|
+
count: async ({ model, where }) => {
|
|
330
|
+
if (getDefaultModelName(model) === "user") {
|
|
331
|
+
model = "$users"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const entities = await fetchEntities({ db, model, where, debugLog })
|
|
335
|
+
|
|
336
|
+
return entities.length
|
|
337
|
+
},
|
|
338
|
+
createSchema: async ({ file = "./auth.schema.ts", tables }) => {
|
|
339
|
+
const code = createSchema(tables, usePlural)
|
|
340
|
+
return { code, path: file }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function fetchEntities({
|
|
348
|
+
db,
|
|
349
|
+
debugLog,
|
|
350
|
+
model,
|
|
351
|
+
where,
|
|
352
|
+
limit,
|
|
353
|
+
offset,
|
|
354
|
+
sortBy
|
|
355
|
+
}: {
|
|
356
|
+
db: InstantAdminDatabase<any, any>
|
|
357
|
+
debugLog: (...args: any[]) => void
|
|
358
|
+
model: string
|
|
359
|
+
where?: Where[]
|
|
360
|
+
limit?: number
|
|
361
|
+
offset?: number
|
|
362
|
+
sortBy?: { field: string; direction: "asc" | "desc" }
|
|
363
|
+
}) {
|
|
364
|
+
let order: Order | undefined
|
|
365
|
+
if (sortBy) {
|
|
366
|
+
order = {
|
|
367
|
+
[sortBy.field]: sortBy.direction
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const query = {
|
|
372
|
+
[model]: { $: { where: parseWhere(where), limit, offset, order } }
|
|
373
|
+
} as InstaQLParams<any>
|
|
374
|
+
|
|
375
|
+
debugLog("Query", prettyObject(query))
|
|
376
|
+
|
|
377
|
+
const result = await db.query(query)
|
|
378
|
+
|
|
379
|
+
debugLog("Result", prettyObject(result))
|
|
380
|
+
|
|
381
|
+
return result[model] as any[]
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function parseWhere(where?: Where[]) {
|
|
385
|
+
const whereQuery = {} as Record<string, unknown>
|
|
386
|
+
where?.forEach((item) => {
|
|
387
|
+
switch (item.operator) {
|
|
388
|
+
case "eq":
|
|
389
|
+
whereQuery[item.field] = item.value
|
|
390
|
+
break
|
|
391
|
+
case "in":
|
|
392
|
+
whereQuery[item.field] = { $in: item.value }
|
|
393
|
+
break
|
|
394
|
+
case "contains":
|
|
395
|
+
whereQuery[item.field] = { $like: `%${item.value}%` }
|
|
396
|
+
break
|
|
397
|
+
case "starts_with":
|
|
398
|
+
whereQuery[item.field] = { $like: `${item.value}%` }
|
|
399
|
+
break
|
|
400
|
+
case "ends_with":
|
|
401
|
+
whereQuery[item.field] = { $like: `%${item.value}` }
|
|
402
|
+
break
|
|
403
|
+
case "ne":
|
|
404
|
+
whereQuery[item.field] = { $not: item.value }
|
|
405
|
+
break
|
|
406
|
+
case "gt":
|
|
407
|
+
whereQuery[item.field] = { $gt: item.value }
|
|
408
|
+
break
|
|
409
|
+
case "gte":
|
|
410
|
+
whereQuery[item.field] = { $gte: item.value }
|
|
411
|
+
break
|
|
412
|
+
case "lt":
|
|
413
|
+
whereQuery[item.field] = { $lt: item.value }
|
|
414
|
+
break
|
|
415
|
+
case "lte":
|
|
416
|
+
whereQuery[item.field] = { $lte: item.value }
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
return whereQuery
|
|
422
|
+
}
|
package/src/index.ts
ADDED
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import util from "node:util"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pretty an object.
|
|
5
|
+
* @param object - The object to pretty.
|
|
6
|
+
* @returns The pretty object.
|
|
7
|
+
*/
|
|
8
|
+
export function prettyObject(object: unknown) {
|
|
9
|
+
return util.inspect(object, { colors: true, depth: null })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts a field name to a relationship label
|
|
14
|
+
* e.g., "userId" -> "user", "organizationId" -> "organization"
|
|
15
|
+
* If field doesn't end with "id", uses the target model name
|
|
16
|
+
*/
|
|
17
|
+
export function fieldNameToLabel(fieldName: string, targetModel: string): string {
|
|
18
|
+
// Remove "Id" suffix if present
|
|
19
|
+
if (fieldName.toLowerCase().endsWith("id")) {
|
|
20
|
+
return fieldName.slice(0, -2)
|
|
21
|
+
}
|
|
22
|
+
// If it doesn't end with "id", use the target model name
|
|
23
|
+
return targetModel
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InstantReactWebDatabase } from "@instantdb/react"
|
|
2
|
+
import type { MinimalAuthClient, SessionResult } from "./types"
|
|
3
|
+
import { useInstantAuth } from "./use-instant-auth"
|
|
4
|
+
|
|
5
|
+
export function InstantAuth<TSessionResult extends SessionResult>({
|
|
6
|
+
db,
|
|
7
|
+
authClient,
|
|
8
|
+
persistent
|
|
9
|
+
}: {
|
|
10
|
+
db: InstantReactWebDatabase<any, any>
|
|
11
|
+
authClient: MinimalAuthClient<TSessionResult>
|
|
12
|
+
persistent?: boolean
|
|
13
|
+
}) {
|
|
14
|
+
useInstantAuth({ db, authClient, persistent })
|
|
15
|
+
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { createAuthClient } from "better-auth/react"
|
|
2
|
+
|
|
3
|
+
export type SessionResult = ReturnType<AuthClient["useSession"]>
|
|
4
|
+
|
|
5
|
+
export type MinimalAuthClient<TSessionResult extends SessionResult> = {
|
|
6
|
+
useSession: () => TSessionResult
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AuthClient = ReturnType<typeof createAuthClient>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { InstantReactWebDatabase } from "@instantdb/react"
|
|
2
|
+
import { useEffect } from "react"
|
|
3
|
+
|
|
4
|
+
import { instantAuth } from "../shared/instant-auth"
|
|
5
|
+
import type { MinimalAuthClient, SessionResult } from "./types"
|
|
6
|
+
import { usePersistentSession } from "./use-persistent-session"
|
|
7
|
+
|
|
8
|
+
export interface InstantAuthProps<TSessionResult extends SessionResult> {
|
|
9
|
+
db: InstantReactWebDatabase<any, any>
|
|
10
|
+
authClient: MinimalAuthClient<TSessionResult>
|
|
11
|
+
persistent?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useInstantAuth<TSessionResult extends SessionResult>({
|
|
15
|
+
db,
|
|
16
|
+
authClient,
|
|
17
|
+
persistent
|
|
18
|
+
}: InstantAuthProps<TSessionResult>) {
|
|
19
|
+
const { isPending, data } = persistent
|
|
20
|
+
? usePersistentSession(authClient)
|
|
21
|
+
: authClient.useSession()
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (isPending) return
|
|
25
|
+
|
|
26
|
+
instantAuth(db, data?.session)
|
|
27
|
+
}, [db, isPending, data?.session])
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BetterFetchError } from "better-auth/react"
|
|
2
|
+
|
|
3
|
+
import type { SessionResult } from "./types"
|
|
4
|
+
import type { InstantAuthProps } from "./use-instant-auth"
|
|
5
|
+
import { usePersistentSession } from "./use-persistent-session"
|
|
6
|
+
|
|
7
|
+
export function useInstantSession<TSessionResult extends SessionResult>({
|
|
8
|
+
db,
|
|
9
|
+
authClient,
|
|
10
|
+
persistent
|
|
11
|
+
}: InstantAuthProps<TSessionResult>) {
|
|
12
|
+
const {
|
|
13
|
+
data: sessionData,
|
|
14
|
+
isPending,
|
|
15
|
+
error,
|
|
16
|
+
isRefetching,
|
|
17
|
+
...rest
|
|
18
|
+
} = persistent ? usePersistentSession(authClient) : authClient.useSession()
|
|
19
|
+
|
|
20
|
+
const { user: authUser, error: authError } = db.useAuth()
|
|
21
|
+
const authPending = sessionData && !authUser && !authError
|
|
22
|
+
|
|
23
|
+
const { data } = db.useQuery(
|
|
24
|
+
authUser
|
|
25
|
+
? {
|
|
26
|
+
$users: { $: { where: { id: authUser.id } } }
|
|
27
|
+
}
|
|
28
|
+
: null
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (data?.$users?.length) {
|
|
32
|
+
const user = data.$users[0]
|
|
33
|
+
|
|
34
|
+
if (sessionData?.user?.id === user.id) {
|
|
35
|
+
sessionData.user = user as typeof sessionData.user
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
data: !authError && !authPending ? sessionData : null,
|
|
41
|
+
isPending: authPending || isPending,
|
|
42
|
+
isRefetching: authPending || isRefetching,
|
|
43
|
+
error: (authError as BetterFetchError) || error,
|
|
44
|
+
...rest
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
|
|
3
|
+
import type { MinimalAuthClient, SessionResult } from "./types"
|
|
4
|
+
import { useHydrated } from "./use-hydrated"
|
|
5
|
+
|
|
6
|
+
let lastPersisted: any | undefined | null
|
|
7
|
+
let restoredData: any | undefined | null
|
|
8
|
+
|
|
9
|
+
export function usePersistentSession<
|
|
10
|
+
TSessionResult extends SessionResult,
|
|
11
|
+
TAuthClient extends MinimalAuthClient<TSessionResult>
|
|
12
|
+
>(authClient: TAuthClient) {
|
|
13
|
+
const { data, isPending, isRefetching, error, ...rest } =
|
|
14
|
+
authClient.useSession()
|
|
15
|
+
const hydrated = useHydrated()
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isPending) return
|
|
19
|
+
|
|
20
|
+
const persistSession = () => {
|
|
21
|
+
if (!data || lastPersisted?.session.id === data?.session.id) return
|
|
22
|
+
|
|
23
|
+
lastPersisted = data
|
|
24
|
+
localStorage.setItem("ba-instant-session", JSON.stringify(data))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const unpersistSession = () => {
|
|
28
|
+
if (data || error || (lastPersisted === null && restoredData === null))
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
localStorage.removeItem("ba-instant-session")
|
|
32
|
+
lastPersisted = null
|
|
33
|
+
restoredData = null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
persistSession()
|
|
37
|
+
unpersistSession()
|
|
38
|
+
}, [data, isPending, error])
|
|
39
|
+
|
|
40
|
+
if (hydrated && !data) {
|
|
41
|
+
if (restoredData === undefined) {
|
|
42
|
+
const persisted = localStorage.getItem("ba-instant-session")
|
|
43
|
+
|
|
44
|
+
if (persisted) {
|
|
45
|
+
const data = JSON.parse(persisted) as TSessionResult
|
|
46
|
+
restoredData = data
|
|
47
|
+
} else {
|
|
48
|
+
restoredData = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (restoredData) {
|
|
53
|
+
return {
|
|
54
|
+
data: restoredData,
|
|
55
|
+
isPending: false,
|
|
56
|
+
isRefetching: false,
|
|
57
|
+
error: null,
|
|
58
|
+
...rest
|
|
59
|
+
} as TSessionResult
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { data, isPending, isRefetching, error, ...rest }
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InstantCoreDatabase } from "@instantdb/core"
|
|
2
|
+
import type { InstantReactWebDatabase } from "@instantdb/react"
|
|
3
|
+
import type { Session } from "better-auth"
|
|
4
|
+
|
|
5
|
+
export async function instantAuth(
|
|
6
|
+
db: InstantCoreDatabase<any, any> | InstantReactWebDatabase<any, any>,
|
|
7
|
+
session?: Session
|
|
8
|
+
) {
|
|
9
|
+
const user = await db.getAuth()
|
|
10
|
+
|
|
11
|
+
if (session && user?.id !== session?.userId) {
|
|
12
|
+
db.auth.signInWithToken(session.token)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!session && user) {
|
|
16
|
+
db.auth.signOut()
|
|
17
|
+
}
|
|
18
|
+
}
|