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,21 @@
|
|
|
1
|
+
export const instantDBPluginClient = ({ authClient }) => {
|
|
2
|
+
// Store the original useSession hook
|
|
3
|
+
const originalUseSession = authClient.useSession;
|
|
4
|
+
// Override useSession with custom implementation using Object.defineProperty
|
|
5
|
+
// This ensures it works even if useSession is a getter or non-writable property
|
|
6
|
+
Object.defineProperty(authClient, "useSession", {
|
|
7
|
+
value: () => {
|
|
8
|
+
// Call the original hook to get session data
|
|
9
|
+
const sessionResult = originalUseSession.call(authClient);
|
|
10
|
+
// You can add custom logic here
|
|
11
|
+
// For example, sync with InstantDB, transform data, etc.
|
|
12
|
+
console.log("useSession override called!", sessionResult);
|
|
13
|
+
// Return the session result (or modified version)
|
|
14
|
+
return sessionResult;
|
|
15
|
+
},
|
|
16
|
+
writable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
enumerable: true
|
|
19
|
+
});
|
|
20
|
+
return authClient;
|
|
21
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InstantReactWebDatabase } from "@instantdb/react";
|
|
2
|
+
import type { MinimalAuthClient, SessionResult } from "./types";
|
|
3
|
+
export declare function InstantAuth<TSessionResult extends SessionResult>({ db, authClient, persistent }: {
|
|
4
|
+
db: InstantReactWebDatabase<any, any>;
|
|
5
|
+
authClient: MinimalAuthClient<TSessionResult>;
|
|
6
|
+
persistent?: boolean;
|
|
7
|
+
}): null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { createAuthClient } from "better-auth/react";
|
|
2
|
+
export type SessionResult = ReturnType<AuthClient["useSession"]>;
|
|
3
|
+
export type MinimalAuthClient<TSessionResult extends SessionResult> = {
|
|
4
|
+
useSession: () => TSessionResult;
|
|
5
|
+
};
|
|
6
|
+
export type AuthClient = ReturnType<typeof createAuthClient>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useHydrated(): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InstantReactWebDatabase } from "@instantdb/react";
|
|
2
|
+
import type { MinimalAuthClient, SessionResult } from "./types";
|
|
3
|
+
export interface InstantAuthProps<TSessionResult extends SessionResult> {
|
|
4
|
+
db: InstantReactWebDatabase<any, any>;
|
|
5
|
+
authClient: MinimalAuthClient<TSessionResult>;
|
|
6
|
+
persistent?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function useInstantAuth<TSessionResult extends SessionResult>({ db, authClient, persistent }: InstantAuthProps<TSessionResult>): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { instantAuth } from "../shared/instant-auth";
|
|
3
|
+
import { usePersistentSession } from "./use-persistent-session";
|
|
4
|
+
export function useInstantAuth({ db, authClient, persistent }) {
|
|
5
|
+
const { isPending, data } = persistent
|
|
6
|
+
? usePersistentSession(authClient)
|
|
7
|
+
: authClient.useSession();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (isPending)
|
|
10
|
+
return;
|
|
11
|
+
instantAuth(db, data === null || data === void 0 ? void 0 : data.session);
|
|
12
|
+
}, [db, isPending, data === null || data === void 0 ? void 0 : data.session]);
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BetterFetchError } from "better-auth/react";
|
|
2
|
+
import type { SessionResult } from "./types";
|
|
3
|
+
import type { InstantAuthProps } from "./use-instant-auth";
|
|
4
|
+
export declare function useInstantSession<TSessionResult extends SessionResult>({ db, authClient, persistent }: InstantAuthProps<TSessionResult>): {
|
|
5
|
+
refetch: (queryParams?: {
|
|
6
|
+
query?: import("better-auth/types").SessionQueryParams;
|
|
7
|
+
}) => void;
|
|
8
|
+
data: {
|
|
9
|
+
user: {
|
|
10
|
+
id: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
email: string;
|
|
14
|
+
emailVerified: boolean;
|
|
15
|
+
name: string;
|
|
16
|
+
image?: string | null | undefined;
|
|
17
|
+
};
|
|
18
|
+
session: {
|
|
19
|
+
id: string;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
userId: string;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
token: string;
|
|
25
|
+
ipAddress?: string | null | undefined;
|
|
26
|
+
userAgent?: string | null | undefined;
|
|
27
|
+
};
|
|
28
|
+
} | null;
|
|
29
|
+
isPending: boolean;
|
|
30
|
+
isRefetching: boolean;
|
|
31
|
+
error: BetterFetchError;
|
|
32
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { usePersistentSession } from "./use-persistent-session";
|
|
2
|
+
export function useInstantSession({ db, authClient, persistent }) {
|
|
3
|
+
var _a, _b;
|
|
4
|
+
const { data: sessionData, isPending, error, isRefetching, ...rest } = persistent ? usePersistentSession(authClient) : authClient.useSession();
|
|
5
|
+
const { user: authUser, error: authError } = db.useAuth();
|
|
6
|
+
const authPending = sessionData && !authUser && !authError;
|
|
7
|
+
const { data } = db.useQuery(authUser
|
|
8
|
+
? {
|
|
9
|
+
$users: { $: { where: { id: authUser.id } } }
|
|
10
|
+
}
|
|
11
|
+
: null);
|
|
12
|
+
if ((_a = data === null || data === void 0 ? void 0 : data.$users) === null || _a === void 0 ? void 0 : _a.length) {
|
|
13
|
+
const user = data.$users[0];
|
|
14
|
+
if (((_b = sessionData === null || sessionData === void 0 ? void 0 : sessionData.user) === null || _b === void 0 ? void 0 : _b.id) === user.id) {
|
|
15
|
+
sessionData.user = user;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
data: !authError && !authPending ? sessionData : null,
|
|
20
|
+
isPending: authPending || isPending,
|
|
21
|
+
isRefetching: authPending || isRefetching,
|
|
22
|
+
error: authError || error,
|
|
23
|
+
...rest
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MinimalAuthClient, SessionResult } from "./types";
|
|
2
|
+
export declare function usePersistentSession<TSessionResult extends SessionResult, TAuthClient extends MinimalAuthClient<TSessionResult>>(authClient: TAuthClient): TSessionResult | ({
|
|
3
|
+
data: {
|
|
4
|
+
user: {
|
|
5
|
+
id: string;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
updatedAt: Date;
|
|
8
|
+
email: string;
|
|
9
|
+
emailVerified: boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
image?: string | null | undefined;
|
|
12
|
+
};
|
|
13
|
+
session: {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
userId: string;
|
|
18
|
+
expiresAt: Date;
|
|
19
|
+
token: string;
|
|
20
|
+
ipAddress?: string | null | undefined;
|
|
21
|
+
userAgent?: string | null | undefined;
|
|
22
|
+
};
|
|
23
|
+
} | null;
|
|
24
|
+
isPending: boolean;
|
|
25
|
+
isRefetching: boolean;
|
|
26
|
+
error: import("better-auth/react").BetterFetchError | null;
|
|
27
|
+
} & Omit<TSessionResult, "data" | "error" | "isPending" | "isRefetching">);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useHydrated } from "./use-hydrated";
|
|
3
|
+
let lastPersisted;
|
|
4
|
+
let restoredData;
|
|
5
|
+
export function usePersistentSession(authClient) {
|
|
6
|
+
const { data, isPending, isRefetching, error, ...rest } = authClient.useSession();
|
|
7
|
+
const hydrated = useHydrated();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (isPending)
|
|
10
|
+
return;
|
|
11
|
+
const persistSession = () => {
|
|
12
|
+
if (!data || (lastPersisted === null || lastPersisted === void 0 ? void 0 : lastPersisted.session.id) === (data === null || data === void 0 ? void 0 : data.session.id))
|
|
13
|
+
return;
|
|
14
|
+
lastPersisted = data;
|
|
15
|
+
localStorage.setItem("ba-instant-session", JSON.stringify(data));
|
|
16
|
+
};
|
|
17
|
+
const unpersistSession = () => {
|
|
18
|
+
if (data || error || (lastPersisted === null && restoredData === null))
|
|
19
|
+
return;
|
|
20
|
+
localStorage.removeItem("ba-instant-session");
|
|
21
|
+
lastPersisted = null;
|
|
22
|
+
restoredData = null;
|
|
23
|
+
};
|
|
24
|
+
persistSession();
|
|
25
|
+
unpersistSession();
|
|
26
|
+
}, [data, isPending, error]);
|
|
27
|
+
if (hydrated && !data) {
|
|
28
|
+
if (restoredData === undefined) {
|
|
29
|
+
const persisted = localStorage.getItem("ba-instant-session");
|
|
30
|
+
if (persisted) {
|
|
31
|
+
const data = JSON.parse(persisted);
|
|
32
|
+
restoredData = data;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
restoredData = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (restoredData) {
|
|
39
|
+
return {
|
|
40
|
+
data: restoredData,
|
|
41
|
+
isPending: false,
|
|
42
|
+
isRefetching: false,
|
|
43
|
+
error: null,
|
|
44
|
+
...rest
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { data, isPending, isRefetching, error, ...rest };
|
|
49
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
let authLoading = false;
|
|
2
|
+
let auth = null;
|
|
3
|
+
export const withInstant = (
|
|
4
|
+
// biome-ignore lint/suspicious/noExplicitAny: any thing goes
|
|
5
|
+
db, authClient) => {
|
|
6
|
+
if (typeof window === "undefined")
|
|
7
|
+
return authClient;
|
|
8
|
+
db.core.subscribeAuth((authResult) => {
|
|
9
|
+
auth = authResult;
|
|
10
|
+
});
|
|
11
|
+
authClient.$store.atoms.session.subscribe(async (value, oldValue) => {
|
|
12
|
+
const { isPending, data, error } = value;
|
|
13
|
+
if (isPending || error) {
|
|
14
|
+
const cachedSessionResult = localStorage.getItem("cached-session-result");
|
|
15
|
+
if (cachedSessionResult && !cachedSessionResult.error) {
|
|
16
|
+
authClient.$store.atoms.session.set(JSON.parse(cachedSessionResult));
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
while (authLoading) {
|
|
21
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
22
|
+
}
|
|
23
|
+
const user = auth === null || auth === void 0 ? void 0 : auth.user;
|
|
24
|
+
if ((user === null || user === void 0 ? void 0 : user.id) === (data === null || data === void 0 ? void 0 : data.user.id))
|
|
25
|
+
return;
|
|
26
|
+
if (user && !data) {
|
|
27
|
+
authLoading = true;
|
|
28
|
+
console.log("signing out");
|
|
29
|
+
await db.auth.signOut();
|
|
30
|
+
authLoading = false;
|
|
31
|
+
}
|
|
32
|
+
if (data) {
|
|
33
|
+
authLoading = true;
|
|
34
|
+
authClient.$store.atoms.session.set(oldValue);
|
|
35
|
+
console.log("signing in with token", data.session.token);
|
|
36
|
+
await db.auth.signInWithToken(data.session.token);
|
|
37
|
+
authClient.$store.atoms.session.set(value);
|
|
38
|
+
authLoading = false;
|
|
39
|
+
}
|
|
40
|
+
localStorage.setItem("cached-session-result", JSON.stringify(value));
|
|
41
|
+
});
|
|
42
|
+
const cachedSessionResult = localStorage.getItem("cached-session-result");
|
|
43
|
+
if (cachedSessionResult) {
|
|
44
|
+
authClient.$store.atoms.session.set(JSON.parse(cachedSessionResult));
|
|
45
|
+
}
|
|
46
|
+
return authClient;
|
|
47
|
+
};
|
package/dist/react.d.ts
ADDED
package/dist/react.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { InstantCoreDatabase } from "@instantdb/core";
|
|
2
|
+
import type { InstantReactWebDatabase } from "@instantdb/react";
|
|
3
|
+
import type { Session } from "better-auth";
|
|
4
|
+
export declare function instantAuth(db: InstantCoreDatabase<any, any> | InstantReactWebDatabase<any, any>, session?: Session): Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function instantAuth(db, session) {
|
|
2
|
+
const user = await db.getAuth();
|
|
3
|
+
if (session && (user === null || user === void 0 ? void 0 : user.id) !== (session === null || session === void 0 ? void 0 : session.userId)) {
|
|
4
|
+
db.auth.signInWithToken(session.token);
|
|
5
|
+
}
|
|
6
|
+
if (!session && user) {
|
|
7
|
+
db.auth.signOut();
|
|
8
|
+
}
|
|
9
|
+
}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-auth-instantdb",
|
|
3
|
+
"homepage": "https://github.com/daveycodez/better-auth-instantdb",
|
|
4
|
+
"version": "1.3.0",
|
|
5
|
+
"description": "Better Auth InstantDB Adapter",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsc --watch",
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"types": "./dist/react/index.d.ts",
|
|
18
|
+
"import": "./dist/react/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"typescript",
|
|
27
|
+
"instantdb",
|
|
28
|
+
"better",
|
|
29
|
+
"better-auth",
|
|
30
|
+
"auth",
|
|
31
|
+
"authentication",
|
|
32
|
+
"adapter"
|
|
33
|
+
],
|
|
34
|
+
"author": "daveycodez",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@better-auth/core": "^1.3.34",
|
|
38
|
+
"@instantdb/admin": "^0.22.51",
|
|
39
|
+
"@instantdb/core": "^0.22.51",
|
|
40
|
+
"@instantdb/react": "^0.22.51",
|
|
41
|
+
"@types/node": "^24.10.1",
|
|
42
|
+
"@types/react": "^19.2.5",
|
|
43
|
+
"@types/react-dom": "^19.2.3",
|
|
44
|
+
"better-auth": "^1.3.34",
|
|
45
|
+
"react": "^19.2.0",
|
|
46
|
+
"react-dom": "^19.2.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@instantdb/admin": ">=0.22.0",
|
|
51
|
+
"better-auth": ">=1.3.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesOptional": {
|
|
54
|
+
"@instantdb/react": ">=0.22.0",
|
|
55
|
+
"react": ">=18.0.0",
|
|
56
|
+
"react-dom": ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@instantdb/react": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"react-dom": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"packageManager": "pnpm@10.22.0"
|
|
70
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { BetterAuthDBSchema, DBFieldAttribute } from "@better-auth/core/db"
|
|
2
|
+
|
|
3
|
+
import { fieldNameToLabel } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a Better Auth field type to InstantDB field type
|
|
7
|
+
*/
|
|
8
|
+
function convertFieldType(
|
|
9
|
+
field: DBFieldAttribute,
|
|
10
|
+
modelName: string,
|
|
11
|
+
fieldKey: string
|
|
12
|
+
) {
|
|
13
|
+
const { type, required, unique, sortable } = field
|
|
14
|
+
|
|
15
|
+
// Handle type as string or array
|
|
16
|
+
const typeStr = Array.isArray(type) ? type[0] : type
|
|
17
|
+
|
|
18
|
+
let fieldType: string
|
|
19
|
+
|
|
20
|
+
switch (typeStr) {
|
|
21
|
+
case "string":
|
|
22
|
+
fieldType = "i.string()"
|
|
23
|
+
break
|
|
24
|
+
case "boolean":
|
|
25
|
+
fieldType = "i.boolean()"
|
|
26
|
+
break
|
|
27
|
+
case "date":
|
|
28
|
+
fieldType = "i.date()"
|
|
29
|
+
break
|
|
30
|
+
case "number":
|
|
31
|
+
fieldType = "i.number()"
|
|
32
|
+
break
|
|
33
|
+
case "json":
|
|
34
|
+
fieldType = "i.json()"
|
|
35
|
+
break
|
|
36
|
+
case "number[]":
|
|
37
|
+
fieldType = "i.json()"
|
|
38
|
+
break
|
|
39
|
+
case "string[]":
|
|
40
|
+
fieldType = "i.json()"
|
|
41
|
+
break
|
|
42
|
+
default:
|
|
43
|
+
fieldType = "i.string()" // Default to string for unknown types
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Apply modifiers
|
|
47
|
+
if (unique) {
|
|
48
|
+
fieldType += ".unique()"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Only make optional if required is explicitly false
|
|
52
|
+
// If required is true, never make it optional (even if defaultValue exists)
|
|
53
|
+
if (required === false) {
|
|
54
|
+
fieldType += ".optional()"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add indexed if sortable
|
|
58
|
+
if (sortable) {
|
|
59
|
+
fieldType += ".indexed()"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For user model, ensure all fields except email end with optional()
|
|
63
|
+
if (
|
|
64
|
+
modelName === "user" &&
|
|
65
|
+
fieldKey !== "email" &&
|
|
66
|
+
!fieldType.endsWith(".optional()")
|
|
67
|
+
) {
|
|
68
|
+
fieldType += ".optional()"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return fieldType
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the InstantDB entity name for a given model name
|
|
76
|
+
*/
|
|
77
|
+
function getEntityName(
|
|
78
|
+
modelName: string,
|
|
79
|
+
tableKey: string,
|
|
80
|
+
usePlural: boolean
|
|
81
|
+
): string {
|
|
82
|
+
if (modelName === "user") {
|
|
83
|
+
return "$users"
|
|
84
|
+
}
|
|
85
|
+
return usePlural ? `${tableKey}s` : tableKey
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Converts a table/model name to camelCase for link names
|
|
90
|
+
*/
|
|
91
|
+
function toCamelCase(str: string): string {
|
|
92
|
+
return str.charAt(0).toLowerCase() + str.slice(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates InstantDB links from Better Auth schema references
|
|
97
|
+
*/
|
|
98
|
+
export function createLinks(
|
|
99
|
+
tables: BetterAuthDBSchema,
|
|
100
|
+
usePlural: boolean
|
|
101
|
+
): Record<string, any> {
|
|
102
|
+
const links: Record<string, any> = {}
|
|
103
|
+
const entityNameMap: Record<string, string> = {}
|
|
104
|
+
|
|
105
|
+
// First pass: build entity name mapping
|
|
106
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
107
|
+
const { modelName } = table
|
|
108
|
+
entityNameMap[modelName] = getEntityName(modelName, key, usePlural)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Second pass: find all references and create links
|
|
112
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
113
|
+
const { modelName, fields } = table
|
|
114
|
+
const sourceEntityName = getEntityName(modelName, key, usePlural)
|
|
115
|
+
|
|
116
|
+
for (const [fieldKey, field] of Object.entries(fields)) {
|
|
117
|
+
const { references } = field
|
|
118
|
+
|
|
119
|
+
if (references) {
|
|
120
|
+
const { model: targetModel, onDelete } = references
|
|
121
|
+
const targetEntityName = entityNameMap[targetModel]
|
|
122
|
+
|
|
123
|
+
if (!targetEntityName) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`Warning: Could not find entity name for model "${targetModel}" referenced by ${modelName}.${fieldKey}`
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Generate forward label from field name, using target model if field doesn't end with "id"
|
|
131
|
+
const forwardLabel = fieldNameToLabel(fieldKey, targetModel)
|
|
132
|
+
|
|
133
|
+
// Generate link name: {sourceTable}{forwardLabel}
|
|
134
|
+
// e.g., "invitations" + "Inviter" -> "invitationsInviter"
|
|
135
|
+
const sourceTableName = sourceEntityName.replace("$", "")
|
|
136
|
+
const forwardLabelCapitalized =
|
|
137
|
+
forwardLabel.charAt(0).toUpperCase() +
|
|
138
|
+
toCamelCase(forwardLabel.slice(1))
|
|
139
|
+
const linkName = `${sourceTableName}${forwardLabelCapitalized}`
|
|
140
|
+
|
|
141
|
+
// Generate reverse label (use source entity name without $ prefix)
|
|
142
|
+
const reverseLabel = sourceEntityName.replace("$", "")
|
|
143
|
+
|
|
144
|
+
// Create link definition
|
|
145
|
+
links[linkName] = {
|
|
146
|
+
forward: {
|
|
147
|
+
on: sourceEntityName,
|
|
148
|
+
has: "one",
|
|
149
|
+
label: forwardLabel,
|
|
150
|
+
onDelete: onDelete || "cascade"
|
|
151
|
+
},
|
|
152
|
+
reverse: {
|
|
153
|
+
on: targetEntityName,
|
|
154
|
+
has: "many",
|
|
155
|
+
label: reverseLabel
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return links
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates an InstantDB schema file from Better Auth schema format
|
|
167
|
+
*/
|
|
168
|
+
export function createSchema(
|
|
169
|
+
tables: BetterAuthDBSchema,
|
|
170
|
+
usePlural: boolean
|
|
171
|
+
): string {
|
|
172
|
+
const entities: Record<string, string> = {}
|
|
173
|
+
|
|
174
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
175
|
+
const { modelName, fields } = table
|
|
176
|
+
|
|
177
|
+
// For other tables, use the key as entity name
|
|
178
|
+
const entityFields: string[] = []
|
|
179
|
+
|
|
180
|
+
for (const [fieldKey, field] of Object.entries(fields)) {
|
|
181
|
+
const fieldType = convertFieldType(field, modelName, fieldKey)
|
|
182
|
+
entityFields.push(`${fieldKey}: ${fieldType}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Pluralize table name if usePlural is true
|
|
186
|
+
const namespace =
|
|
187
|
+
modelName === "user" ? "$users" : usePlural ? `${key}s` : key
|
|
188
|
+
entities[namespace] =
|
|
189
|
+
`i.entity({\n ${entityFields.join(",\n ")}\n })`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Generate links from references
|
|
193
|
+
const links = createLinks(tables, usePlural)
|
|
194
|
+
|
|
195
|
+
// Generate the schema file content
|
|
196
|
+
const entitiesString = Object.entries(entities)
|
|
197
|
+
.map(([name, definition]) => ` ${name}: ${definition}`)
|
|
198
|
+
.join(",\n")
|
|
199
|
+
|
|
200
|
+
// Format links as string
|
|
201
|
+
const linksString = Object.entries(links)
|
|
202
|
+
.map(([linkName, linkDef]) => {
|
|
203
|
+
const forward = linkDef.forward
|
|
204
|
+
const reverse = linkDef.reverse
|
|
205
|
+
return ` ${linkName}: {
|
|
206
|
+
forward: {
|
|
207
|
+
on: "${forward.on}",
|
|
208
|
+
has: "${forward.has}",
|
|
209
|
+
label: "${forward.label}"${forward.onDelete ? `,\n onDelete: "${forward.onDelete}"` : ""}
|
|
210
|
+
},
|
|
211
|
+
reverse: {
|
|
212
|
+
on: "${reverse.on}",
|
|
213
|
+
has: "${reverse.has}",
|
|
214
|
+
label: "${reverse.label}"
|
|
215
|
+
}
|
|
216
|
+
}`
|
|
217
|
+
})
|
|
218
|
+
.join(",\n")
|
|
219
|
+
|
|
220
|
+
const linksSection = linksString ? `,\n links: {\n${linksString}\n }` : ""
|
|
221
|
+
|
|
222
|
+
return `// Docs: https://www.instantdb.com/docs/modeling-data
|
|
223
|
+
|
|
224
|
+
import { i } from "@instantdb/react"
|
|
225
|
+
|
|
226
|
+
export const authSchema = i.schema({
|
|
227
|
+
entities: {
|
|
228
|
+
${entitiesString}
|
|
229
|
+
}${linksSection}
|
|
230
|
+
})
|
|
231
|
+
`
|
|
232
|
+
}
|