jazz-tools 0.18.14 → 0.18.16
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/.turbo/turbo-build.log +45 -33
- package/CHANGELOG.md +23 -0
- package/dist/better-auth/database-adapter/index.d.ts +50 -0
- package/dist/better-auth/database-adapter/index.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/index.js +920 -0
- package/dist/better-auth/database-adapter/index.js.map +1 -0
- package/dist/better-auth/database-adapter/repository/account.d.ts +24 -0
- package/dist/better-auth/database-adapter/repository/account.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/generic.d.ts +45 -0
- package/dist/better-auth/database-adapter/repository/generic.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/index.d.ts +6 -0
- package/dist/better-auth/database-adapter/repository/index.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/session.d.ts +29 -0
- package/dist/better-auth/database-adapter/repository/session.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/user.d.ts +30 -0
- package/dist/better-auth/database-adapter/repository/user.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/verification.d.ts +18 -0
- package/dist/better-auth/database-adapter/repository/verification.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/schema.d.ts +27 -0
- package/dist/better-auth/database-adapter/schema.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/index.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/index.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/account.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/account.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/generic.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/generic.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/session.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/session.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/user.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/user.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/verification.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/verification.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/sync-utils.d.ts +16 -0
- package/dist/better-auth/database-adapter/tests/sync-utils.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/utils.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/utils.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/utils.d.ts +16 -0
- package/dist/better-auth/database-adapter/utils.d.ts.map +1 -0
- package/dist/worker/edge-wasm.d.ts +2 -0
- package/dist/worker/edge-wasm.d.ts.map +1 -0
- package/dist/worker/edge-wasm.js +5 -0
- package/dist/worker/edge-wasm.js.map +1 -0
- package/jazz-tools-0.18.6.tgz +0 -0
- package/package.json +15 -5
- package/src/better-auth/database-adapter/index.ts +228 -0
- package/src/better-auth/database-adapter/repository/account.ts +131 -0
- package/src/better-auth/database-adapter/repository/generic.ts +297 -0
- package/src/better-auth/database-adapter/repository/index.ts +5 -0
- package/src/better-auth/database-adapter/repository/session.ts +190 -0
- package/src/better-auth/database-adapter/repository/user.ts +158 -0
- package/src/better-auth/database-adapter/repository/verification.ts +37 -0
- package/src/better-auth/database-adapter/schema.ts +222 -0
- package/src/better-auth/database-adapter/tests/index.test.ts +690 -0
- package/src/better-auth/database-adapter/tests/repository/account.test.ts +149 -0
- package/src/better-auth/database-adapter/tests/repository/generic.test.ts +183 -0
- package/src/better-auth/database-adapter/tests/repository/session.test.ts +419 -0
- package/src/better-auth/database-adapter/tests/repository/user.test.ts +673 -0
- package/src/better-auth/database-adapter/tests/repository/verification.test.ts +101 -0
- package/src/better-auth/database-adapter/tests/sync-utils.ts +127 -0
- package/src/better-auth/database-adapter/tests/utils.test.ts +787 -0
- package/src/better-auth/database-adapter/utils.ts +178 -0
- package/src/worker/edge-wasm.ts +5 -0
- package/tsup.config.ts +8 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
import { type AdapterDebugLogs, createAdapter } from "better-auth/adapters";
|
2
|
+
import type { Account } from "jazz-tools";
|
3
|
+
import { startWorker } from "jazz-tools/worker";
|
4
|
+
import {
|
5
|
+
JazzRepository,
|
6
|
+
UserRepository,
|
7
|
+
SessionRepository,
|
8
|
+
VerificationRepository,
|
9
|
+
AccountRepository,
|
10
|
+
} from "./repository";
|
11
|
+
import { createJazzSchema, tableItem2Record } from "./schema.js";
|
12
|
+
|
13
|
+
export interface JazzAdapterConfig {
|
14
|
+
/**
|
15
|
+
* Helps you debug issues with the adapter.
|
16
|
+
*/
|
17
|
+
debugLogs?: AdapterDebugLogs;
|
18
|
+
/**
|
19
|
+
* The sync server to use.
|
20
|
+
*/
|
21
|
+
syncServer: string;
|
22
|
+
/**
|
23
|
+
* The worker account ID to use.
|
24
|
+
*/
|
25
|
+
accountID: string;
|
26
|
+
/**
|
27
|
+
* The worker account secret to use.
|
28
|
+
*/
|
29
|
+
accountSecret: string;
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Creates a Better Auth database adapter that integrates with Jazz framework.
|
34
|
+
*
|
35
|
+
* This adapter provides a seamless integration between Better Auth and Jazz,
|
36
|
+
* allowing you to use Jazz as database for for Better Auth's authentication system.
|
37
|
+
*
|
38
|
+
* @param config - Configuration object for the Jazz Better Auth adapter
|
39
|
+
* @param config.syncServer - The Jazz sync server URL to connect to (e.g., "wss://your-sync-server.com")
|
40
|
+
* @param config.accountID - The worker account ID for the Jazz worker that will handle auth operations
|
41
|
+
* @param config.accountSecret - The worker account secret for authenticating with the Jazz sync server
|
42
|
+
* @param config.debugLogs - Optional debug logging configuration to help troubleshoot adapter issues
|
43
|
+
*
|
44
|
+
* @returns A Better Auth adapter instance configured to work with Jazz
|
45
|
+
*
|
46
|
+
* @example
|
47
|
+
* ```typescript
|
48
|
+
* import { JazzBetterAuthDatabaseAdapter } from "jazz-tools/better-auth/database-adapter";
|
49
|
+
* import { createAuth } from "better-auth";
|
50
|
+
*
|
51
|
+
* const auth = createAuth({
|
52
|
+
* adapter: JazzBetterAuthDatabaseAdapter({
|
53
|
+
* syncServer: "wss://your-jazz-sync-server.com",
|
54
|
+
* accountID: "auth-worker-account-id",
|
55
|
+
* accountSecret: "your-worker-account-secret",
|
56
|
+
* }),
|
57
|
+
* // ... other auth configuration
|
58
|
+
* });
|
59
|
+
* ```
|
60
|
+
*/
|
61
|
+
export const JazzBetterAuthDatabaseAdapter = (
|
62
|
+
config: JazzAdapterConfig,
|
63
|
+
): ReturnType<typeof createAdapter> =>
|
64
|
+
createAdapter({
|
65
|
+
config: {
|
66
|
+
adapterId: "jazz-tools-adapter", // A unique identifier for the adapter.
|
67
|
+
adapterName: "Jazz Tools Adapter", // The name of the adapter.
|
68
|
+
debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.
|
69
|
+
supportsJSON: true, // Whether the database supports JSON. (Default: false)
|
70
|
+
supportsDates: true, // Whether the database supports dates. (Default: true)
|
71
|
+
supportsBooleans: true, // Whether the database supports booleans. (Default: true)
|
72
|
+
supportsNumericIds: false, // Whether the database supports auto-incrementing numeric IDs. (Default: true)
|
73
|
+
disableIdGeneration: true,
|
74
|
+
},
|
75
|
+
adapter: ({ schema }) => {
|
76
|
+
const JazzSchema = createJazzSchema(schema);
|
77
|
+
|
78
|
+
let worker: Account | undefined = undefined;
|
79
|
+
|
80
|
+
async function getWorker() {
|
81
|
+
if (worker) {
|
82
|
+
return worker;
|
83
|
+
}
|
84
|
+
|
85
|
+
const result = await startWorker({
|
86
|
+
AccountSchema: JazzSchema.WorkerAccount,
|
87
|
+
syncServer: config.syncServer,
|
88
|
+
accountID: config.accountID,
|
89
|
+
accountSecret: config.accountSecret,
|
90
|
+
skipInboxLoad: true,
|
91
|
+
asActiveAccount: false,
|
92
|
+
});
|
93
|
+
|
94
|
+
worker = result.worker;
|
95
|
+
|
96
|
+
return worker;
|
97
|
+
}
|
98
|
+
|
99
|
+
async function initRepository(
|
100
|
+
model: string,
|
101
|
+
ensureSync: boolean = false,
|
102
|
+
): Promise<JazzRepository> {
|
103
|
+
let Repository: typeof JazzRepository | undefined = undefined;
|
104
|
+
switch (model) {
|
105
|
+
case "user":
|
106
|
+
Repository = UserRepository;
|
107
|
+
break;
|
108
|
+
case "session":
|
109
|
+
Repository = SessionRepository;
|
110
|
+
break;
|
111
|
+
case "verification":
|
112
|
+
Repository = VerificationRepository;
|
113
|
+
break;
|
114
|
+
case "account":
|
115
|
+
Repository = AccountRepository;
|
116
|
+
break;
|
117
|
+
}
|
118
|
+
|
119
|
+
if (!Repository) {
|
120
|
+
Repository = JazzRepository;
|
121
|
+
}
|
122
|
+
|
123
|
+
const worker = await getWorker();
|
124
|
+
const database = await JazzSchema.loadDatabase(worker);
|
125
|
+
|
126
|
+
const repository = new Repository(
|
127
|
+
JazzSchema.DatabaseRoot,
|
128
|
+
database,
|
129
|
+
worker,
|
130
|
+
schema,
|
131
|
+
ensureSync,
|
132
|
+
);
|
133
|
+
|
134
|
+
return repository;
|
135
|
+
}
|
136
|
+
|
137
|
+
return {
|
138
|
+
create: async ({ data, model, select }): Promise<any> => {
|
139
|
+
// console.log("create", { data, model, select });
|
140
|
+
const repository = await initRepository(model, true);
|
141
|
+
|
142
|
+
const created = await repository.create(model, data);
|
143
|
+
|
144
|
+
await repository.ensureSync();
|
145
|
+
|
146
|
+
return tableItem2Record(created);
|
147
|
+
},
|
148
|
+
update: async ({ model, where, update }): Promise<any> => {
|
149
|
+
// console.log("update", { model, where, update });
|
150
|
+
const repository = await initRepository(model, true);
|
151
|
+
|
152
|
+
const updated = await repository.update(
|
153
|
+
model,
|
154
|
+
where,
|
155
|
+
update as Record<string, any>,
|
156
|
+
);
|
157
|
+
|
158
|
+
if (updated.length === 0) {
|
159
|
+
return null;
|
160
|
+
}
|
161
|
+
|
162
|
+
await repository.ensureSync();
|
163
|
+
|
164
|
+
return tableItem2Record(updated[0]!);
|
165
|
+
},
|
166
|
+
updateMany: async ({ model, where, update }) => {
|
167
|
+
// console.log("updateMany", { model, where, update });
|
168
|
+
const repository = await initRepository(model, true);
|
169
|
+
|
170
|
+
const updated = await repository.update(model, where, update);
|
171
|
+
|
172
|
+
await repository.ensureSync();
|
173
|
+
|
174
|
+
return updated.length;
|
175
|
+
},
|
176
|
+
delete: async ({ model, where }) => {
|
177
|
+
// console.log("delete", { model, where });
|
178
|
+
const repository = await initRepository(model, true);
|
179
|
+
|
180
|
+
await repository.deleteValue(model, where);
|
181
|
+
|
182
|
+
await repository.ensureSync();
|
183
|
+
},
|
184
|
+
findOne: async ({ model, where }): Promise<any> => {
|
185
|
+
// console.log("findOne", { model, where });
|
186
|
+
const repository = await initRepository(model);
|
187
|
+
|
188
|
+
const item = await repository.findOne(model, where);
|
189
|
+
|
190
|
+
return tableItem2Record(item);
|
191
|
+
},
|
192
|
+
findMany: async ({
|
193
|
+
model,
|
194
|
+
where,
|
195
|
+
limit,
|
196
|
+
sortBy,
|
197
|
+
offset,
|
198
|
+
}): Promise<any[]> => {
|
199
|
+
const repository = await initRepository(model);
|
200
|
+
|
201
|
+
const items = await repository.findMany(
|
202
|
+
model,
|
203
|
+
where,
|
204
|
+
limit,
|
205
|
+
sortBy,
|
206
|
+
offset,
|
207
|
+
);
|
208
|
+
|
209
|
+
return items.map(tableItem2Record);
|
210
|
+
},
|
211
|
+
deleteMany: async ({ model, where }) => {
|
212
|
+
const repository = await initRepository(model, true);
|
213
|
+
|
214
|
+
const deleted = await repository.deleteValue(model, where);
|
215
|
+
|
216
|
+
await repository.ensureSync();
|
217
|
+
|
218
|
+
return deleted;
|
219
|
+
},
|
220
|
+
count: async ({ model, where }) => {
|
221
|
+
// console.log("count", { model, where });
|
222
|
+
const repository = await initRepository(model);
|
223
|
+
|
224
|
+
return repository.count(model, where);
|
225
|
+
},
|
226
|
+
};
|
227
|
+
},
|
228
|
+
});
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { CleanedWhere } from "better-auth/adapters";
|
2
|
+
import { co, z } from "jazz-tools";
|
3
|
+
import { JazzRepository } from "./generic";
|
4
|
+
import { isWhereBySingleField } from "../utils";
|
5
|
+
import type { TableItem } from "../schema";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* This list stores an array of covalues ID
|
9
|
+
* mapped by the accountID property.
|
10
|
+
* Because accountId is not unique, and it is used on every social login
|
11
|
+
*/
|
12
|
+
const AccountIdIndex = co.list(z.string());
|
13
|
+
|
14
|
+
export class AccountRepository extends JazzRepository {
|
15
|
+
/**
|
16
|
+
* Custom logic:
|
17
|
+
* - keep sync accountId index
|
18
|
+
*/
|
19
|
+
async create(
|
20
|
+
model: string,
|
21
|
+
data: Record<string, any>,
|
22
|
+
uniqueId?: string,
|
23
|
+
): Promise<TableItem> {
|
24
|
+
const account = await super.create(model, data, uniqueId);
|
25
|
+
|
26
|
+
await this.updateAccountIdIndex(
|
27
|
+
account[this.getAccountIdProperty()],
|
28
|
+
account.$jazz.id,
|
29
|
+
);
|
30
|
+
|
31
|
+
return account;
|
32
|
+
}
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Custom logic:
|
36
|
+
* - if the accountId is in the where clause, get the ids from the index
|
37
|
+
*/
|
38
|
+
async findMany(
|
39
|
+
model: string,
|
40
|
+
where: CleanedWhere[] | undefined,
|
41
|
+
limit?: number,
|
42
|
+
sortBy?: { field: string; direction: "asc" | "desc" },
|
43
|
+
offset?: number,
|
44
|
+
): Promise<TableItem[]> {
|
45
|
+
if (isWhereBySingleField(this.getAccountIdProperty(), where)) {
|
46
|
+
const accountIdIndex = await this.getAccountIdIndex(where[0].value);
|
47
|
+
|
48
|
+
const ids = accountIdIndex ?? [];
|
49
|
+
|
50
|
+
if (ids.length === 0) {
|
51
|
+
return [];
|
52
|
+
}
|
53
|
+
|
54
|
+
// except for accountId clashing from different social providers,
|
55
|
+
// ids should contain a single id, max two
|
56
|
+
const results = await Promise.all(
|
57
|
+
ids.map((id) =>
|
58
|
+
super.findById(model, [
|
59
|
+
{ field: "id", operator: "eq", value: id, connector: "AND" },
|
60
|
+
]),
|
61
|
+
),
|
62
|
+
);
|
63
|
+
|
64
|
+
return results.filter((value) => value !== null);
|
65
|
+
}
|
66
|
+
|
67
|
+
return super.findMany(model, where, limit, sortBy, offset);
|
68
|
+
}
|
69
|
+
|
70
|
+
async deleteValue(model: string, where: CleanedWhere[]): Promise<number> {
|
71
|
+
const nodes = await this.findMany(model, where);
|
72
|
+
|
73
|
+
const deleted = await super.deleteValue(model, where);
|
74
|
+
|
75
|
+
for (const node of nodes) {
|
76
|
+
const accountId = node.$jazz.raw.get(this.getAccountIdProperty()) as
|
77
|
+
| string
|
78
|
+
| undefined;
|
79
|
+
|
80
|
+
if (accountId) {
|
81
|
+
await this.deleteAccountIdIndex(accountId, node.$jazz.id);
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
return deleted;
|
86
|
+
}
|
87
|
+
|
88
|
+
private async getAccountIdIndex(accountId: string) {
|
89
|
+
const indexUnique = this.getAccountIdProperty() + "_" + accountId;
|
90
|
+
|
91
|
+
const accountIdIndex = await AccountIdIndex.loadUnique(
|
92
|
+
indexUnique,
|
93
|
+
this.owner.$jazz.id,
|
94
|
+
{
|
95
|
+
loadAs: this.worker,
|
96
|
+
},
|
97
|
+
);
|
98
|
+
|
99
|
+
return accountIdIndex;
|
100
|
+
}
|
101
|
+
|
102
|
+
private async updateAccountIdIndex(accountId: string, entityId: string) {
|
103
|
+
const accountIdIndex = await this.getAccountIdIndex(accountId);
|
104
|
+
|
105
|
+
const ids = accountIdIndex ?? [];
|
106
|
+
|
107
|
+
await AccountIdIndex.upsertUnique({
|
108
|
+
value: [...ids, entityId],
|
109
|
+
unique: this.getAccountIdProperty() + "_" + accountId,
|
110
|
+
owner: this.owner,
|
111
|
+
});
|
112
|
+
}
|
113
|
+
|
114
|
+
private async deleteAccountIdIndex(accountId: string, entityId: string) {
|
115
|
+
const accountIdIndex = await this.getAccountIdIndex(accountId);
|
116
|
+
|
117
|
+
const ids = accountIdIndex ?? [];
|
118
|
+
|
119
|
+
await AccountIdIndex.upsertUnique({
|
120
|
+
value: ids.filter((id) => id !== entityId),
|
121
|
+
unique: this.getAccountIdProperty() + "_" + accountId,
|
122
|
+
owner: this.owner,
|
123
|
+
});
|
124
|
+
}
|
125
|
+
|
126
|
+
private getAccountIdProperty(): string {
|
127
|
+
return (
|
128
|
+
this.betterAuthSchema.account?.fields.accountId?.fieldName || "accountId"
|
129
|
+
);
|
130
|
+
}
|
131
|
+
}
|
@@ -0,0 +1,297 @@
|
|
1
|
+
import { CleanedWhere } from "better-auth/adapters";
|
2
|
+
import { BetterAuthDbSchema } from "better-auth/db";
|
3
|
+
import { Account, CoList, CoMap, Group, co } from "jazz-tools";
|
4
|
+
import type { Database, TableItem } from "../schema.js";
|
5
|
+
import {
|
6
|
+
filterListByWhere,
|
7
|
+
isWhereBySingleField,
|
8
|
+
paginateList,
|
9
|
+
sortListByField,
|
10
|
+
} from "../utils.js";
|
11
|
+
|
12
|
+
export class JazzRepository {
|
13
|
+
protected databaseSchema: Database;
|
14
|
+
protected databaseRoot: co.loaded<Database, { group: true }>;
|
15
|
+
protected worker: Account;
|
16
|
+
protected owner: Group;
|
17
|
+
protected betterAuthSchema: BetterAuthDbSchema;
|
18
|
+
|
19
|
+
private coValuesTracker:
|
20
|
+
| {
|
21
|
+
done: () => Set<`co_z${string}`>;
|
22
|
+
}
|
23
|
+
| undefined = undefined;
|
24
|
+
|
25
|
+
constructor(
|
26
|
+
databaseSchema: Database,
|
27
|
+
databaseRoot: co.loaded<Database, { group: true }>,
|
28
|
+
worker: Account,
|
29
|
+
betterAuthSchema: BetterAuthDbSchema = {},
|
30
|
+
ensureSync: boolean = false,
|
31
|
+
) {
|
32
|
+
this.databaseSchema = databaseSchema;
|
33
|
+
this.databaseRoot = databaseRoot;
|
34
|
+
this.worker = worker;
|
35
|
+
this.owner = databaseRoot.group;
|
36
|
+
this.betterAuthSchema = betterAuthSchema;
|
37
|
+
|
38
|
+
if (ensureSync)
|
39
|
+
this.coValuesTracker =
|
40
|
+
worker.$jazz.raw.core.node.syncManager.trackDirtyCoValues();
|
41
|
+
}
|
42
|
+
|
43
|
+
ensureSync() {
|
44
|
+
if (!this.coValuesTracker)
|
45
|
+
throw new Error("Repository wasn't initialized with ensureSync option");
|
46
|
+
|
47
|
+
return Promise.all(
|
48
|
+
Array.from(this.coValuesTracker.done(), (id) =>
|
49
|
+
this.worker.$jazz.raw.core.node.syncManager.waitForSync(id),
|
50
|
+
),
|
51
|
+
);
|
52
|
+
}
|
53
|
+
|
54
|
+
async create(
|
55
|
+
model: string,
|
56
|
+
data: Record<string, any>,
|
57
|
+
uniqueId?: string,
|
58
|
+
): Promise<TableItem> {
|
59
|
+
const schema = this.getSchema(model);
|
60
|
+
|
61
|
+
const resolved = await this.databaseRoot.$jazz.ensureLoaded({
|
62
|
+
resolve: {
|
63
|
+
tables: {
|
64
|
+
[model]: true,
|
65
|
+
},
|
66
|
+
},
|
67
|
+
});
|
68
|
+
|
69
|
+
const list = resolved.tables?.[model] as unknown as CoList<CoMap>;
|
70
|
+
|
71
|
+
if (!uniqueId) {
|
72
|
+
// Use the same owner of the table.
|
73
|
+
const node = schema.create(data, {
|
74
|
+
owner: list.$jazz.owner,
|
75
|
+
});
|
76
|
+
|
77
|
+
list.$jazz.push(node);
|
78
|
+
|
79
|
+
return node;
|
80
|
+
}
|
81
|
+
|
82
|
+
// If we have a unique id, we must check for soft deleted items first
|
83
|
+
const existingNode = (await schema.loadUnique(
|
84
|
+
uniqueId,
|
85
|
+
list.$jazz.owner.$jazz.id,
|
86
|
+
{
|
87
|
+
loadAs: this.worker,
|
88
|
+
},
|
89
|
+
)) as CoMap | null;
|
90
|
+
|
91
|
+
// if the entity exists and is not soft deleted, we must throw an error
|
92
|
+
if (existingNode && existingNode.$jazz?.raw.get("_deleted") !== true) {
|
93
|
+
throw new Error("Entity already exists");
|
94
|
+
}
|
95
|
+
|
96
|
+
// create the entity or update the deleted one
|
97
|
+
const node = await schema.upsertUnique({
|
98
|
+
value: {
|
99
|
+
...data,
|
100
|
+
_deleted: false,
|
101
|
+
},
|
102
|
+
owner: list.$jazz.owner,
|
103
|
+
unique: uniqueId,
|
104
|
+
});
|
105
|
+
|
106
|
+
if (!node) {
|
107
|
+
throw new Error("Unable to create entity");
|
108
|
+
}
|
109
|
+
|
110
|
+
list.$jazz.push(node);
|
111
|
+
|
112
|
+
return node;
|
113
|
+
}
|
114
|
+
|
115
|
+
async findOne(
|
116
|
+
model: string,
|
117
|
+
where: CleanedWhere[],
|
118
|
+
): Promise<TableItem | null> {
|
119
|
+
return this.findMany(model, where, 1).then((users) => users?.at(0) ?? null);
|
120
|
+
}
|
121
|
+
|
122
|
+
async findById(
|
123
|
+
model: string,
|
124
|
+
where: [{ field: "id"; operator: "eq"; value: string; connector: "AND" }],
|
125
|
+
): Promise<TableItem | null> {
|
126
|
+
const id = where[0]!.value;
|
127
|
+
|
128
|
+
if (!id.startsWith("co_")) {
|
129
|
+
return null;
|
130
|
+
}
|
131
|
+
|
132
|
+
const node = await this.getSchema(model).load(id, { loadAs: this.worker });
|
133
|
+
|
134
|
+
if (!node) {
|
135
|
+
return null;
|
136
|
+
}
|
137
|
+
|
138
|
+
if (node.$jazz.raw.get("_deleted")) {
|
139
|
+
return null;
|
140
|
+
}
|
141
|
+
|
142
|
+
return node;
|
143
|
+
}
|
144
|
+
|
145
|
+
async findByUnique(
|
146
|
+
model: string,
|
147
|
+
where: [{ field: string; operator: "eq"; value: string; connector: "AND" }],
|
148
|
+
): Promise<TableItem | null> {
|
149
|
+
const value = where[0]!.value;
|
150
|
+
|
151
|
+
const node = await this.getSchema(model).loadUnique(
|
152
|
+
value,
|
153
|
+
this.owner.$jazz.id,
|
154
|
+
{
|
155
|
+
loadAs: this.worker,
|
156
|
+
},
|
157
|
+
);
|
158
|
+
|
159
|
+
if (!node) {
|
160
|
+
return null;
|
161
|
+
}
|
162
|
+
|
163
|
+
if (node.$jazz.raw.get("_deleted")) {
|
164
|
+
return null;
|
165
|
+
}
|
166
|
+
|
167
|
+
return node;
|
168
|
+
}
|
169
|
+
|
170
|
+
async findMany(
|
171
|
+
model: string,
|
172
|
+
where: CleanedWhere[] | undefined,
|
173
|
+
limit?: number,
|
174
|
+
sortBy?: { field: string; direction: "asc" | "desc" },
|
175
|
+
offset?: number,
|
176
|
+
): Promise<TableItem[]> {
|
177
|
+
// ensure schema exists
|
178
|
+
this.getSchema(model);
|
179
|
+
|
180
|
+
if (isWhereBySingleField("id", where)) {
|
181
|
+
return this.findById(model, where).then((node) => (node ? [node] : []));
|
182
|
+
}
|
183
|
+
|
184
|
+
const resolvedRoot = await this.databaseRoot.$jazz.ensureLoaded({
|
185
|
+
resolve: {
|
186
|
+
tables: {
|
187
|
+
[model]: {
|
188
|
+
$each: true,
|
189
|
+
},
|
190
|
+
},
|
191
|
+
},
|
192
|
+
});
|
193
|
+
|
194
|
+
const list = resolvedRoot.tables?.[model] as CoList<CoMap> | undefined;
|
195
|
+
|
196
|
+
if (!list) {
|
197
|
+
return [];
|
198
|
+
}
|
199
|
+
|
200
|
+
return this.filterSortPaginateList(list, where, limit, sortBy, offset);
|
201
|
+
}
|
202
|
+
|
203
|
+
async update(
|
204
|
+
model: string,
|
205
|
+
where: CleanedWhere[],
|
206
|
+
update: Record<string, any>,
|
207
|
+
): Promise<TableItem[]> {
|
208
|
+
const nodes = await this.findMany(model, where);
|
209
|
+
|
210
|
+
if (nodes.length === 0) {
|
211
|
+
return [];
|
212
|
+
}
|
213
|
+
|
214
|
+
for (const node of nodes) {
|
215
|
+
for (const [key, value] of Object.entries(
|
216
|
+
update as Record<string, any>,
|
217
|
+
)) {
|
218
|
+
node.$jazz.set(key, value);
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
return nodes;
|
223
|
+
}
|
224
|
+
|
225
|
+
async deleteValue(model: string, where: CleanedWhere[]): Promise<number> {
|
226
|
+
const items = await this.findMany(model, where);
|
227
|
+
|
228
|
+
if (items.length === 0) {
|
229
|
+
return 0;
|
230
|
+
}
|
231
|
+
|
232
|
+
const resolved = await this.databaseRoot.$jazz.ensureLoaded({
|
233
|
+
resolve: {
|
234
|
+
tables: {
|
235
|
+
[model]: {
|
236
|
+
$each: true,
|
237
|
+
},
|
238
|
+
},
|
239
|
+
},
|
240
|
+
});
|
241
|
+
|
242
|
+
if (!resolved) {
|
243
|
+
throw new Error("Unable to load values");
|
244
|
+
}
|
245
|
+
|
246
|
+
const list = resolved?.tables?.[model] as unknown as CoList<CoMap>;
|
247
|
+
|
248
|
+
for (const toBeDeleted of items) {
|
249
|
+
// Get entries without trigger the shallow load
|
250
|
+
const index = [...list.entries()].findIndex(
|
251
|
+
([_, value]) => value && value.$jazz.id === toBeDeleted.$jazz.id,
|
252
|
+
);
|
253
|
+
|
254
|
+
toBeDeleted.$jazz.set("_deleted", true);
|
255
|
+
|
256
|
+
if (index !== -1) {
|
257
|
+
list.$jazz.remove(index);
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
return items.length;
|
262
|
+
}
|
263
|
+
|
264
|
+
async count(
|
265
|
+
model: string,
|
266
|
+
where: CleanedWhere[] | undefined,
|
267
|
+
): Promise<number> {
|
268
|
+
return this.findMany(model, where).then((values) => values.length);
|
269
|
+
}
|
270
|
+
|
271
|
+
protected getSchema(model: string) {
|
272
|
+
const schema = this.databaseSchema.shape.tables.shape[model]?.element;
|
273
|
+
if (!schema) {
|
274
|
+
throw new Error(`Schema for model "${model}" not found`);
|
275
|
+
}
|
276
|
+
return schema;
|
277
|
+
}
|
278
|
+
|
279
|
+
protected filterSortPaginateList<T extends TableItem>(
|
280
|
+
list: CoList<CoMap | null>,
|
281
|
+
where: CleanedWhere[] | undefined,
|
282
|
+
limit?: number,
|
283
|
+
sortBy?: { field: string; direction: "asc" | "desc" },
|
284
|
+
offset?: number,
|
285
|
+
): T[] {
|
286
|
+
// ignore nullable values and soft deleted items
|
287
|
+
return [
|
288
|
+
list.filter(
|
289
|
+
(item) => item !== null && item.$jazz.raw.get("_deleted") !== true,
|
290
|
+
),
|
291
|
+
]
|
292
|
+
.map((list) => filterListByWhere(list, where))
|
293
|
+
.map((list) => sortListByField(list, sortBy))
|
294
|
+
.map((list) => paginateList(list, limit, offset))
|
295
|
+
.at(0)! as T[];
|
296
|
+
}
|
297
|
+
}
|