@indexeddb-orm/idb-orm 0.0.1
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/.vscode/extensions.json +5 -0
- package/README.md +1280 -0
- package/angular-demo-app/README.md +84 -0
- package/angular-demo-app/angular.json +109 -0
- package/angular-demo-app/package-lock.json +14215 -0
- package/angular-demo-app/package.json +41 -0
- package/angular-demo-app/src/app/app.component.ts +481 -0
- package/angular-demo-app/src/app/app.routes.ts +8 -0
- package/angular-demo-app/src/app/components/actions.component.ts +202 -0
- package/angular-demo-app/src/app/components/cloud-sync-demo.component.ts +296 -0
- package/angular-demo-app/src/app/components/live-query-demo.component.ts +307 -0
- package/angular-demo-app/src/app/components/main-info.component.ts +148 -0
- package/angular-demo-app/src/app/components/posts-live-query-demo.component.ts +336 -0
- package/angular-demo-app/src/app/components/typescript-demo.component.ts +268 -0
- package/angular-demo-app/src/entities/post-tag.entity.ts +25 -0
- package/angular-demo-app/src/entities/post.entity.ts +49 -0
- package/angular-demo-app/src/entities/profile.entity.ts +42 -0
- package/angular-demo-app/src/entities/tag.entity.ts +36 -0
- package/angular-demo-app/src/entities/user.entity.ts +59 -0
- package/angular-demo-app/src/favicon.ico +1 -0
- package/angular-demo-app/src/index.html +16 -0
- package/angular-demo-app/src/main.ts +13 -0
- package/angular-demo-app/src/services/app-logic.service.ts +449 -0
- package/angular-demo-app/src/services/cloud-sync.service.ts +95 -0
- package/angular-demo-app/src/services/database.service.ts +26 -0
- package/angular-demo-app/src/services/live-query.service.ts +63 -0
- package/angular-demo-app/src/services/posts-live-query.service.ts +86 -0
- package/angular-demo-app/src/services/typescript-demo.service.ts +59 -0
- package/angular-demo-app/src/styles.scss +50 -0
- package/angular-demo-app/tsconfig.app.json +13 -0
- package/angular-demo-app/tsconfig.json +34 -0
- package/angular-demo-app/tsconfig.spec.json +13 -0
- package/dist/Database.d.ts +206 -0
- package/dist/Database.js +288 -0
- package/dist/decorators/Column.d.ts +79 -0
- package/dist/decorators/Column.js +236 -0
- package/dist/decorators/Entity.d.ts +32 -0
- package/dist/decorators/Entity.js +44 -0
- package/dist/decorators/Relation.d.ts +70 -0
- package/dist/decorators/Relation.js +120 -0
- package/dist/decorators/index.d.ts +3 -0
- package/dist/decorators/index.js +3 -0
- package/dist/errors/ValidationError.d.ts +4 -0
- package/dist/errors/ValidationError.js +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/metadata/Column.d.ts +8 -0
- package/dist/metadata/Column.js +44 -0
- package/dist/metadata/Entity.d.ts +11 -0
- package/dist/metadata/Entity.js +21 -0
- package/dist/metadata/Relation.d.ts +20 -0
- package/dist/metadata/Relation.js +74 -0
- package/dist/metadata/index.d.ts +3 -0
- package/dist/metadata/index.js +3 -0
- package/dist/services/AggregationService.d.ts +38 -0
- package/dist/services/AggregationService.js +229 -0
- package/dist/services/BaseEntity.d.ts +32 -0
- package/dist/services/BaseEntity.js +62 -0
- package/dist/services/CloudSyncService.d.ts +100 -0
- package/dist/services/CloudSyncService.js +196 -0
- package/dist/services/DecoratorUtils.d.ts +12 -0
- package/dist/services/DecoratorUtils.js +10 -0
- package/dist/services/EntityFactory.d.ts +25 -0
- package/dist/services/EntityFactory.js +27 -0
- package/dist/services/EntityRegistry.d.ts +61 -0
- package/dist/services/EntityRegistry.js +56 -0
- package/dist/services/EntitySchema.d.ts +56 -0
- package/dist/services/EntitySchema.js +125 -0
- package/dist/services/MigrationManager.d.ts +70 -0
- package/dist/services/MigrationManager.js +181 -0
- package/dist/services/RelationLoader.d.ts +66 -0
- package/dist/services/RelationLoader.js +310 -0
- package/dist/services/SchemaBuilder.d.ts +68 -0
- package/dist/services/SchemaBuilder.js +191 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.js +7 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +16 -0
- package/eslint.config.js +49 -0
- package/homepage/favicon.svg +36 -0
- package/homepage/index.html +1725 -0
- package/package.json +78 -0
- package/react-demo-app/README.md +61 -0
- package/react-demo-app/eslint.config.js +60 -0
- package/react-demo-app/index.html +13 -0
- package/react-demo-app/package-lock.json +4955 -0
- package/react-demo-app/package.json +39 -0
- package/react-demo-app/src/App.tsx +172 -0
- package/react-demo-app/src/assets/react.svg +1 -0
- package/react-demo-app/src/components/Actions.tsx +171 -0
- package/react-demo-app/src/components/CloudSyncDemo.tsx +191 -0
- package/react-demo-app/src/components/LiveQueryDemo.tsx +122 -0
- package/react-demo-app/src/components/MainInfo.tsx +75 -0
- package/react-demo-app/src/components/PostsLiveQueryDemo.tsx +185 -0
- package/react-demo-app/src/components/TypeScriptDemo.tsx +190 -0
- package/react-demo-app/src/database/Database.ts +30 -0
- package/react-demo-app/src/entities/Post.ts +48 -0
- package/react-demo-app/src/entities/PostTag.ts +26 -0
- package/react-demo-app/src/entities/Profile.ts +41 -0
- package/react-demo-app/src/entities/Tag.ts +35 -0
- package/react-demo-app/src/entities/User.ts +61 -0
- package/react-demo-app/src/hooks/useAppLogic.ts +565 -0
- package/react-demo-app/src/hooks/useCloudSyncDemo.ts +84 -0
- package/react-demo-app/src/hooks/useLiveQueryDemo.ts +68 -0
- package/react-demo-app/src/hooks/usePostsLiveQueryDemo.ts +64 -0
- package/react-demo-app/src/hooks/useTypeScriptDemo.ts +43 -0
- package/react-demo-app/src/index.css +26 -0
- package/react-demo-app/src/main.tsx +18 -0
- package/react-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
- package/react-demo-app/src/migrations/002-add-post-category.ts +37 -0
- package/react-demo-app/src/migrations/index.ts +8 -0
- package/react-demo-app/src/vite-env.d.ts +1 -0
- package/react-demo-app/tsconfig.app.json +22 -0
- package/react-demo-app/tsconfig.json +6 -0
- package/react-demo-app/vite.config.ts +10 -0
- package/src/Database.ts +405 -0
- package/src/errors/ValidationError.ts +9 -0
- package/src/index.ts +13 -0
- package/src/metadata/Column.ts +74 -0
- package/src/metadata/Entity.ts +42 -0
- package/src/metadata/Relation.ts +121 -0
- package/src/metadata/index.ts +5 -0
- package/src/services/AggregationService.ts +348 -0
- package/src/services/BaseEntity.ts +77 -0
- package/src/services/CloudSyncService.ts +248 -0
- package/src/services/EntityFactory.ts +35 -0
- package/src/services/EntityRegistry.ts +109 -0
- package/src/services/EntitySchema.ts +154 -0
- package/src/services/MigrationManager.ts +276 -0
- package/src/services/RelationLoader.ts +532 -0
- package/src/services/SchemaBuilder.ts +237 -0
- package/src/services/index.ts +7 -0
- package/src/types.d.ts +1 -0
- package/src/types.ts +169 -0
- package/src/utils/logger.ts +40 -0
- package/svelte-demo-app/README.md +61 -0
- package/svelte-demo-app/package-lock.json +3000 -0
- package/svelte-demo-app/package.json +30 -0
- package/svelte-demo-app/src/app.d.ts +12 -0
- package/svelte-demo-app/src/app.html +13 -0
- package/svelte-demo-app/src/components/Actions.svelte +121 -0
- package/svelte-demo-app/src/components/CloudSyncDemo.svelte +333 -0
- package/svelte-demo-app/src/components/LiveQueryDemo.svelte +191 -0
- package/svelte-demo-app/src/components/MainInfo.svelte +133 -0
- package/svelte-demo-app/src/components/PostsLiveQueryDemo.svelte +330 -0
- package/svelte-demo-app/src/components/TypeScriptDemo.svelte +251 -0
- package/svelte-demo-app/src/database/Database.ts +29 -0
- package/svelte-demo-app/src/entities/Post.ts +46 -0
- package/svelte-demo-app/src/entities/PostTag.ts +22 -0
- package/svelte-demo-app/src/entities/Profile.ts +39 -0
- package/svelte-demo-app/src/entities/Tag.ts +33 -0
- package/svelte-demo-app/src/entities/User.ts +62 -0
- package/svelte-demo-app/src/lib/database/Database.ts +30 -0
- package/svelte-demo-app/src/lib/entities/Post.ts +47 -0
- package/svelte-demo-app/src/lib/entities/PostTag.ts +23 -0
- package/svelte-demo-app/src/lib/entities/Profile.ts +40 -0
- package/svelte-demo-app/src/lib/entities/Tag.ts +34 -0
- package/svelte-demo-app/src/lib/entities/User.ts +59 -0
- package/svelte-demo-app/src/lib/index.ts +7 -0
- package/svelte-demo-app/src/lib/migrations/001-add-user-email-index.ts +17 -0
- package/svelte-demo-app/src/lib/migrations/002-add-post-category.ts +37 -0
- package/svelte-demo-app/src/lib/migrations/index.ts +8 -0
- package/svelte-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
- package/svelte-demo-app/src/migrations/002-add-post-category.ts +37 -0
- package/svelte-demo-app/src/migrations/index.ts +8 -0
- package/svelte-demo-app/src/routes/+layout.js +3 -0
- package/svelte-demo-app/src/routes/+layout.svelte +228 -0
- package/svelte-demo-app/src/routes/+page.js +3 -0
- package/svelte-demo-app/src/routes/+page.svelte +1305 -0
- package/svelte-demo-app/src/stores/appStore.js +603 -0
- package/svelte-demo-app/svelte.config.js +18 -0
- package/svelte-demo-app/tsconfig.json +14 -0
- package/svelte-demo-app/vite.config.ts +6 -0
- package/tests/aggregation.e2e.test.ts +87 -0
- package/tests/base-entity.e2e.test.ts +47 -0
- package/tests/database-api.e2e.test.ts +177 -0
- package/tests/decorators.e2e.test.ts +40 -0
- package/tests/entity-schema.e2e.test.ts +58 -0
- package/tests/relation-loader-table-names.test.ts +192 -0
- package/tests/relations.e2e.test.ts +178 -0
- package/tests/zod-runtime.e2e.test.ts +69 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +21 -0
- package/vitest.setup.ts +27 -0
- package/vue-demo-app/README.md +61 -0
- package/vue-demo-app/index.html +13 -0
- package/vue-demo-app/package-lock.json +1537 -0
- package/vue-demo-app/package.json +27 -0
- package/vue-demo-app/src/App.vue +100 -0
- package/vue-demo-app/src/components/Actions.vue +135 -0
- package/vue-demo-app/src/components/CloudSyncDemo.vue +139 -0
- package/vue-demo-app/src/components/LiveQueryDemo.vue +122 -0
- package/vue-demo-app/src/components/MainInfo.vue +80 -0
- package/vue-demo-app/src/components/PostsLiveQueryDemo.vue +136 -0
- package/vue-demo-app/src/components/TypeScriptDemo.vue +133 -0
- package/vue-demo-app/src/database/Database.ts +29 -0
- package/vue-demo-app/src/entities/Post.ts +48 -0
- package/vue-demo-app/src/entities/PostTag.ts +24 -0
- package/vue-demo-app/src/entities/Profile.ts +41 -0
- package/vue-demo-app/src/entities/Tag.ts +35 -0
- package/vue-demo-app/src/entities/User.ts +61 -0
- package/vue-demo-app/src/main.ts +29 -0
- package/vue-demo-app/src/migrations/001-add-user-email-index.ts +23 -0
- package/vue-demo-app/src/migrations/002-add-post-category.ts +46 -0
- package/vue-demo-app/src/migrations/index.ts +14 -0
- package/vue-demo-app/src/services/useAppLogic.ts +565 -0
- package/vue-demo-app/src/services/useCloudSyncDemo.ts +84 -0
- package/vue-demo-app/src/services/useLiveQueryDemo.ts +82 -0
- package/vue-demo-app/src/services/usePostsLiveQueryDemo.ts +77 -0
- package/vue-demo-app/src/services/useTypeScriptDemo.ts +56 -0
- package/vue-demo-app/src/vite-env.d.ts +1 -0
- package/vue-demo-app/tsconfig.json +25 -0
- package/vue-demo-app/tsconfig.node.json +10 -0
- package/vue-demo-app/vite.config.ts +16 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { getEntityMetadata } from '../metadata/Entity';
|
|
2
|
+
import { getRelationMetadata } from '../metadata/Relation';
|
|
3
|
+
import type { EntityConstructor } from '../types';
|
|
4
|
+
import { BaseEntity } from './BaseEntity';
|
|
5
|
+
|
|
6
|
+
export class RelationLoader {
|
|
7
|
+
private db: {
|
|
8
|
+
table: (_name: string) => unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
constructor(database: {
|
|
12
|
+
table: (_name: string) => unknown;
|
|
13
|
+
}) {
|
|
14
|
+
this.db = database;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private resolveTableName(target: string | EntityConstructor): string {
|
|
18
|
+
if (typeof target === 'string') return target;
|
|
19
|
+
|
|
20
|
+
const meta = getEntityMetadata(target);
|
|
21
|
+
if (meta?.tableName) return meta.tableName;
|
|
22
|
+
|
|
23
|
+
const n = target.name;
|
|
24
|
+
|
|
25
|
+
return (/Entity$/i.test(n)
|
|
26
|
+
? n.replace(/Entity$/i, '')
|
|
27
|
+
: n).toLowerCase() + 's';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load relations for an entity
|
|
32
|
+
* @param entity - The entity to load relations for
|
|
33
|
+
* @param entityClass - The entity class
|
|
34
|
+
* @param relationNames - Optional array of specific relation names to load.
|
|
35
|
+
* If not provided, loads all eager relations.
|
|
36
|
+
*/
|
|
37
|
+
async loadRelations<T extends BaseEntity>(
|
|
38
|
+
entity: T,
|
|
39
|
+
entityClass: EntityConstructor<T>,
|
|
40
|
+
relationNames?: string[],
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
const relations = getRelationMetadata(entityClass);
|
|
43
|
+
|
|
44
|
+
if (!relations) {
|
|
45
|
+
return entity;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const loadedEntity = { ...entity };
|
|
49
|
+
|
|
50
|
+
const relationsToLoad = relationNames
|
|
51
|
+
? relationNames.filter(name => relations[name])
|
|
52
|
+
: Object.entries(relations)
|
|
53
|
+
.filter(([, meta]) => meta.eager)
|
|
54
|
+
.map(([name]) => name);
|
|
55
|
+
|
|
56
|
+
for (const relationName of relationsToLoad) {
|
|
57
|
+
const relationMeta = relations[relationName];
|
|
58
|
+
|
|
59
|
+
if (relationMeta) {
|
|
60
|
+
(loadedEntity as Record<string, unknown>)[relationName] =
|
|
61
|
+
await this.loadRelation(entity, relationMeta);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return loadedEntity;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load a specific relation by name
|
|
70
|
+
* @param entity - The entity to load relation for
|
|
71
|
+
* @param entityClass - The entity class
|
|
72
|
+
* @param relationName - The name of the relation to load
|
|
73
|
+
* @returns Promise resolving to the loaded relation data
|
|
74
|
+
*/
|
|
75
|
+
async loadRelationByName<
|
|
76
|
+
T extends BaseEntity,
|
|
77
|
+
K extends keyof T
|
|
78
|
+
>(
|
|
79
|
+
entity: T,
|
|
80
|
+
entityClass: EntityConstructor<T>,
|
|
81
|
+
relationName: K,
|
|
82
|
+
): Promise<T[K]> {
|
|
83
|
+
const relations = getRelationMetadata(entityClass);
|
|
84
|
+
|
|
85
|
+
if (!relations || !relations[String(relationName as string)]) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Relation
|
|
88
|
+
'${String(relationName)}' not found for entity ${entityClass.name}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const relationMeta = relations[String(relationName as string)];
|
|
93
|
+
|
|
94
|
+
return await this.loadRelation(entity, relationMeta) as T[K];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load a specific relation based on metadata
|
|
99
|
+
* @param entity - The entity to load relation for
|
|
100
|
+
* @param relationMeta - The relation metadata containing:
|
|
101
|
+
* type, target, and configuration
|
|
102
|
+
* @returns Promise resolving to the loaded relation data
|
|
103
|
+
*/
|
|
104
|
+
async loadRelation<T extends BaseEntity>(
|
|
105
|
+
entity: T,
|
|
106
|
+
relationMeta: {
|
|
107
|
+
type: string;
|
|
108
|
+
target: string | EntityConstructor;
|
|
109
|
+
foreignKey?: string;
|
|
110
|
+
joinTable?: string;
|
|
111
|
+
},
|
|
112
|
+
): Promise<unknown> {
|
|
113
|
+
const targetName = this.resolveTableName(relationMeta.target);
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
const targetTable = this.db.table(targetName) as any;
|
|
116
|
+
|
|
117
|
+
switch (relationMeta.type) {
|
|
118
|
+
case 'one-to-one':
|
|
119
|
+
return await this.loadOneToOne(
|
|
120
|
+
entity as Record<string, unknown>,
|
|
121
|
+
targetTable,
|
|
122
|
+
relationMeta,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
case 'one-to-many':
|
|
126
|
+
return await this.loadOneToMany(
|
|
127
|
+
entity as Record<string, unknown>,
|
|
128
|
+
targetTable,
|
|
129
|
+
relationMeta,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
case 'many-to-many':
|
|
133
|
+
return await this.loadManyToMany(
|
|
134
|
+
entity as Record<string, unknown>,
|
|
135
|
+
targetTable,
|
|
136
|
+
relationMeta,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
default:
|
|
140
|
+
throw new Error(`Unsupported relation type: ${relationMeta.type}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async loadOneToOne(
|
|
145
|
+
entity: Record<string, unknown>,
|
|
146
|
+
targetTable: {
|
|
147
|
+
where: (_field: string) => {
|
|
148
|
+
equals: (_value: unknown) => { first: () => Promise<unknown> };
|
|
149
|
+
};
|
|
150
|
+
get: (_id: string | number) => Promise<unknown>;
|
|
151
|
+
},
|
|
152
|
+
relationMeta: { foreignKey?: string },
|
|
153
|
+
): Promise<unknown> {
|
|
154
|
+
if (relationMeta.foreignKey) {
|
|
155
|
+
return await targetTable
|
|
156
|
+
.where(relationMeta.foreignKey)
|
|
157
|
+
.equals(entity.id)
|
|
158
|
+
.first();
|
|
159
|
+
} else {
|
|
160
|
+
const foreignKeyValue = entity[relationMeta.foreignKey as string];
|
|
161
|
+
|
|
162
|
+
if (!foreignKeyValue) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return await targetTable.get(foreignKeyValue as string | number);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async loadOneToMany(
|
|
171
|
+
entity: Record<string, unknown>,
|
|
172
|
+
targetTable: {
|
|
173
|
+
where: (_field: string) => {
|
|
174
|
+
equals: (_value: unknown) => { toArray: () => Promise<unknown[]> };
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
relationMeta: { foreignKey?: string },
|
|
178
|
+
): Promise<unknown[]> {
|
|
179
|
+
if (!relationMeta.foreignKey) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return await targetTable
|
|
184
|
+
.where(relationMeta.foreignKey)
|
|
185
|
+
.equals(entity.id)
|
|
186
|
+
.toArray();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Load many-to-many relation
|
|
191
|
+
*/
|
|
192
|
+
private async loadManyToMany(
|
|
193
|
+
entity: Record<string, unknown>,
|
|
194
|
+
targetTable: {
|
|
195
|
+
where: (_field: string) => {
|
|
196
|
+
anyOf: (_values: unknown[]) => { toArray: () => Promise<unknown[]> };
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
relationMeta: { joinTable?: string },
|
|
200
|
+
): Promise<unknown[]> {
|
|
201
|
+
if (!relationMeta.joinTable) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
+
const joinTable = this.db.table(relationMeta.joinTable) as any;
|
|
207
|
+
const joinRecords = await joinTable
|
|
208
|
+
.where('sourceId')
|
|
209
|
+
.equals(entity.id)
|
|
210
|
+
.toArray();
|
|
211
|
+
const targetIds = joinRecords.map((record: unknown) =>
|
|
212
|
+
(record as Record<string, unknown>).targetId,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (targetIds.length === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return await targetTable
|
|
220
|
+
.where('id')
|
|
221
|
+
.anyOf(targetIds as (string | number)[])
|
|
222
|
+
.toArray();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Save entity with all its relations
|
|
227
|
+
* @param entity - The entity to save with relations
|
|
228
|
+
* @param entityClass - The entity class
|
|
229
|
+
* @returns Promise resolving to the saved entity with relations
|
|
230
|
+
*/
|
|
231
|
+
async saveWithRelations<T extends BaseEntity>(
|
|
232
|
+
entity: T,
|
|
233
|
+
entityClass: EntityConstructor<T>,
|
|
234
|
+
): Promise<T> {
|
|
235
|
+
const relations = getRelationMetadata(entityClass);
|
|
236
|
+
|
|
237
|
+
if (!relations) {
|
|
238
|
+
return await this.saveEntity(entity, entityClass);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const savedEntity = await this.saveEntity(entity, entityClass);
|
|
242
|
+
|
|
243
|
+
for (const [propertyKey, relationMeta] of Object.entries(relations)) {
|
|
244
|
+
const relationData = (entity as Record<string, unknown>)[propertyKey];
|
|
245
|
+
|
|
246
|
+
if (relationData) {
|
|
247
|
+
await this.saveRelation(
|
|
248
|
+
savedEntity as Record<string, unknown>,
|
|
249
|
+
relationData,
|
|
250
|
+
relationMeta,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return savedEntity;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Delete entity with cascade handling for relations
|
|
260
|
+
* @param entity - The entity to delete
|
|
261
|
+
* @param entityClass - The entity class
|
|
262
|
+
*/
|
|
263
|
+
async deleteWithRelations<T extends BaseEntity>(
|
|
264
|
+
entity: T,
|
|
265
|
+
entityClass: EntityConstructor<T>,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
const relations = getRelationMetadata(entityClass);
|
|
268
|
+
|
|
269
|
+
if (relations) {
|
|
270
|
+
for (const [, relationMeta] of Object.entries(relations)) {
|
|
271
|
+
if (!relationMeta.cascade) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await this.deleteRelation(
|
|
276
|
+
entity as unknown as Record<string, unknown>,
|
|
277
|
+
relationMeta as unknown as {
|
|
278
|
+
type: string;
|
|
279
|
+
target: string | EntityConstructor;
|
|
280
|
+
foreignKey?: string;
|
|
281
|
+
joinTable?: string;
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
const baseMeta = getEntityMetadata(entityClass);
|
|
289
|
+
const baseName = baseMeta?.tableName
|
|
290
|
+
|| ((/Entity$/i.test(entityClass.name)
|
|
291
|
+
? entityClass.name.replace(/Entity$/i, '')
|
|
292
|
+
: entityClass.name).toLowerCase() + 's');
|
|
293
|
+
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
|
+
const table = this.db.table(baseName) as any;
|
|
296
|
+
|
|
297
|
+
await table.delete(entity.id as string | number);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async deleteRelation(
|
|
301
|
+
entity: Record<string, unknown>,
|
|
302
|
+
relationMeta: {
|
|
303
|
+
type: string;
|
|
304
|
+
target: string | EntityConstructor;
|
|
305
|
+
foreignKey?: string;
|
|
306
|
+
joinTable?: string;
|
|
307
|
+
},
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const targetName = this.resolveTableName(relationMeta.target);
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
311
|
+
const targetTable = this.db.table(targetName) as any;
|
|
312
|
+
|
|
313
|
+
switch (relationMeta.type) {
|
|
314
|
+
case 'one-to-one':
|
|
315
|
+
await this.deleteOneToOne(entity, targetTable, relationMeta);
|
|
316
|
+
break;
|
|
317
|
+
case 'one-to-many':
|
|
318
|
+
await this.deleteOneToMany(entity, targetTable, relationMeta);
|
|
319
|
+
break;
|
|
320
|
+
case 'many-to-many':
|
|
321
|
+
await this.deleteManyToMany(entity, relationMeta);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async deleteOneToOne(
|
|
327
|
+
entity: Record<string, unknown>,
|
|
328
|
+
targetTable: {
|
|
329
|
+
where: (_field: string) => {
|
|
330
|
+
equals: (_value: unknown) => { delete: () => Promise<unknown> };
|
|
331
|
+
};
|
|
332
|
+
delete: (_id: string | number) => Promise<unknown>;
|
|
333
|
+
get: (_id: string | number) => Promise<unknown>;
|
|
334
|
+
},
|
|
335
|
+
relationMeta: { foreignKey?: string },
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
if (relationMeta.foreignKey) {
|
|
338
|
+
await targetTable
|
|
339
|
+
.where(relationMeta.foreignKey)
|
|
340
|
+
.equals(entity.id)
|
|
341
|
+
.delete();
|
|
342
|
+
} else if (entity.id) {
|
|
343
|
+
const related = await targetTable.get(entity.id as string | number);
|
|
344
|
+
|
|
345
|
+
if (related) {
|
|
346
|
+
await targetTable.delete(entity.id as string | number);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async deleteOneToMany(
|
|
352
|
+
entity: Record<string, unknown>,
|
|
353
|
+
targetTable: {
|
|
354
|
+
where: (_field: string) => {
|
|
355
|
+
equals: (_value: unknown) => { delete: () => Promise<unknown> };
|
|
356
|
+
};
|
|
357
|
+
},
|
|
358
|
+
relationMeta: { foreignKey?: string },
|
|
359
|
+
): Promise<void> {
|
|
360
|
+
if (!relationMeta.foreignKey) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await targetTable
|
|
365
|
+
.where(relationMeta.foreignKey)
|
|
366
|
+
.equals(entity.id)
|
|
367
|
+
.delete();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async deleteManyToMany(
|
|
371
|
+
entity: Record<string, unknown>,
|
|
372
|
+
relationMeta: { joinTable?: string },
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
if (!relationMeta.joinTable) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
+
const joinTable = this.db.table(relationMeta.joinTable) as any;
|
|
380
|
+
|
|
381
|
+
await joinTable
|
|
382
|
+
.where('sourceId')
|
|
383
|
+
.equals(entity.id)
|
|
384
|
+
.delete();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async saveEntity<T extends BaseEntity>(
|
|
388
|
+
entity: T,
|
|
389
|
+
entityClass: EntityConstructor<T>,
|
|
390
|
+
): Promise<T> {
|
|
391
|
+
|
|
392
|
+
const baseMeta2 = getEntityMetadata(entityClass);
|
|
393
|
+
const baseName2 = baseMeta2?.tableName
|
|
394
|
+
|| ((/Entity$/i.test(entityClass.name)
|
|
395
|
+
? entityClass.name.replace(/Entity$/i, '')
|
|
396
|
+
: entityClass.name).toLowerCase() + 's');
|
|
397
|
+
|
|
398
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
399
|
+
const table = this.db.table(baseName2) as any;
|
|
400
|
+
|
|
401
|
+
if (entity.id) {
|
|
402
|
+
await table.put(entity);
|
|
403
|
+
|
|
404
|
+
return entity;
|
|
405
|
+
} else {
|
|
406
|
+
const id = await table.add(entity);
|
|
407
|
+
|
|
408
|
+
return { ...entity, id };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async saveRelation(
|
|
413
|
+
entity: Record<string, unknown>,
|
|
414
|
+
relationData: unknown,
|
|
415
|
+
relationMeta: {
|
|
416
|
+
type: string;
|
|
417
|
+
target: string | EntityConstructor;
|
|
418
|
+
foreignKey?: string;
|
|
419
|
+
joinTable?: string
|
|
420
|
+
},
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
const targetName = this.resolveTableName(relationMeta.target);
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
|
+
const targetTable = this.db.table(targetName) as any;
|
|
425
|
+
|
|
426
|
+
switch (relationMeta.type) {
|
|
427
|
+
case 'one-to-one':
|
|
428
|
+
await this.saveOneToOne(entity, relationData, targetTable, relationMeta);
|
|
429
|
+
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case 'one-to-many':
|
|
433
|
+
await this
|
|
434
|
+
.saveOneToMany(entity, relationData as unknown[], targetTable, relationMeta);
|
|
435
|
+
|
|
436
|
+
break;
|
|
437
|
+
|
|
438
|
+
case 'many-to-many':
|
|
439
|
+
await this.saveManyToMany(
|
|
440
|
+
entity, relationData as unknown[], targetTable, relationMeta,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private async saveOneToOne(
|
|
448
|
+
entity: Record<string, unknown>,
|
|
449
|
+
relationData: unknown,
|
|
450
|
+
targetTable: {
|
|
451
|
+
update: (_id: string | number, _data: unknown) => Promise<unknown>;
|
|
452
|
+
add: (_data: unknown) => Promise<unknown>;
|
|
453
|
+
},
|
|
454
|
+
relationMeta: { foreignKey?: string },
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
const data = relationData as Record<string, unknown>;
|
|
457
|
+
|
|
458
|
+
if (relationMeta.foreignKey) {
|
|
459
|
+
data[relationMeta.foreignKey] = entity.id;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (data.id) {
|
|
463
|
+
await targetTable.update(data.id as string | number, data);
|
|
464
|
+
} else {
|
|
465
|
+
await targetTable.add(data);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async saveOneToMany(
|
|
470
|
+
entity: Record<string, unknown>,
|
|
471
|
+
relationData: unknown[],
|
|
472
|
+
targetTable: {
|
|
473
|
+
update: (_id: string | number, _data: unknown) => Promise<unknown>;
|
|
474
|
+
add: (_data: unknown) => Promise<unknown>;
|
|
475
|
+
},
|
|
476
|
+
relationMeta: { foreignKey?: string },
|
|
477
|
+
): Promise<void> {
|
|
478
|
+
if (!relationMeta.foreignKey) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const item of relationData) {
|
|
483
|
+
const data = item as Record<string, unknown>;
|
|
484
|
+
|
|
485
|
+
data[relationMeta.foreignKey] = entity.id;
|
|
486
|
+
|
|
487
|
+
if (data.id) {
|
|
488
|
+
await targetTable.update(data.id as string | number, data);
|
|
489
|
+
} else {
|
|
490
|
+
await targetTable.add(data);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async saveManyToMany(
|
|
496
|
+
entity: Record<string, unknown>,
|
|
497
|
+
relationData: unknown[],
|
|
498
|
+
targetTable: {
|
|
499
|
+
add: (_data: unknown) => Promise<unknown>;
|
|
500
|
+
update: (_id: string | number, _data: unknown) => Promise<unknown>;
|
|
501
|
+
},
|
|
502
|
+
relationMeta: { joinTable?: string },
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
if (!relationMeta.joinTable) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
509
|
+
const joinTable = this.db.table(relationMeta.joinTable) as any;
|
|
510
|
+
|
|
511
|
+
await joinTable
|
|
512
|
+
.where('sourceId')
|
|
513
|
+
.equals(entity.id)
|
|
514
|
+
.delete();
|
|
515
|
+
|
|
516
|
+
for (const item of relationData) {
|
|
517
|
+
const data = item as Record<string, unknown>;
|
|
518
|
+
let targetId = data.id;
|
|
519
|
+
|
|
520
|
+
if (!targetId) {
|
|
521
|
+
targetId = await targetTable.add(data);
|
|
522
|
+
} else {
|
|
523
|
+
await targetTable.update(data.id as string | number, data);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
await joinTable.add({
|
|
527
|
+
sourceId: entity.id,
|
|
528
|
+
targetId: targetId,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|