@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.
Files changed (216) hide show
  1. package/.vscode/extensions.json +5 -0
  2. package/README.md +1280 -0
  3. package/angular-demo-app/README.md +84 -0
  4. package/angular-demo-app/angular.json +109 -0
  5. package/angular-demo-app/package-lock.json +14215 -0
  6. package/angular-demo-app/package.json +41 -0
  7. package/angular-demo-app/src/app/app.component.ts +481 -0
  8. package/angular-demo-app/src/app/app.routes.ts +8 -0
  9. package/angular-demo-app/src/app/components/actions.component.ts +202 -0
  10. package/angular-demo-app/src/app/components/cloud-sync-demo.component.ts +296 -0
  11. package/angular-demo-app/src/app/components/live-query-demo.component.ts +307 -0
  12. package/angular-demo-app/src/app/components/main-info.component.ts +148 -0
  13. package/angular-demo-app/src/app/components/posts-live-query-demo.component.ts +336 -0
  14. package/angular-demo-app/src/app/components/typescript-demo.component.ts +268 -0
  15. package/angular-demo-app/src/entities/post-tag.entity.ts +25 -0
  16. package/angular-demo-app/src/entities/post.entity.ts +49 -0
  17. package/angular-demo-app/src/entities/profile.entity.ts +42 -0
  18. package/angular-demo-app/src/entities/tag.entity.ts +36 -0
  19. package/angular-demo-app/src/entities/user.entity.ts +59 -0
  20. package/angular-demo-app/src/favicon.ico +1 -0
  21. package/angular-demo-app/src/index.html +16 -0
  22. package/angular-demo-app/src/main.ts +13 -0
  23. package/angular-demo-app/src/services/app-logic.service.ts +449 -0
  24. package/angular-demo-app/src/services/cloud-sync.service.ts +95 -0
  25. package/angular-demo-app/src/services/database.service.ts +26 -0
  26. package/angular-demo-app/src/services/live-query.service.ts +63 -0
  27. package/angular-demo-app/src/services/posts-live-query.service.ts +86 -0
  28. package/angular-demo-app/src/services/typescript-demo.service.ts +59 -0
  29. package/angular-demo-app/src/styles.scss +50 -0
  30. package/angular-demo-app/tsconfig.app.json +13 -0
  31. package/angular-demo-app/tsconfig.json +34 -0
  32. package/angular-demo-app/tsconfig.spec.json +13 -0
  33. package/dist/Database.d.ts +206 -0
  34. package/dist/Database.js +288 -0
  35. package/dist/decorators/Column.d.ts +79 -0
  36. package/dist/decorators/Column.js +236 -0
  37. package/dist/decorators/Entity.d.ts +32 -0
  38. package/dist/decorators/Entity.js +44 -0
  39. package/dist/decorators/Relation.d.ts +70 -0
  40. package/dist/decorators/Relation.js +120 -0
  41. package/dist/decorators/index.d.ts +3 -0
  42. package/dist/decorators/index.js +3 -0
  43. package/dist/errors/ValidationError.d.ts +4 -0
  44. package/dist/errors/ValidationError.js +8 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +7 -0
  47. package/dist/metadata/Column.d.ts +8 -0
  48. package/dist/metadata/Column.js +44 -0
  49. package/dist/metadata/Entity.d.ts +11 -0
  50. package/dist/metadata/Entity.js +21 -0
  51. package/dist/metadata/Relation.d.ts +20 -0
  52. package/dist/metadata/Relation.js +74 -0
  53. package/dist/metadata/index.d.ts +3 -0
  54. package/dist/metadata/index.js +3 -0
  55. package/dist/services/AggregationService.d.ts +38 -0
  56. package/dist/services/AggregationService.js +229 -0
  57. package/dist/services/BaseEntity.d.ts +32 -0
  58. package/dist/services/BaseEntity.js +62 -0
  59. package/dist/services/CloudSyncService.d.ts +100 -0
  60. package/dist/services/CloudSyncService.js +196 -0
  61. package/dist/services/DecoratorUtils.d.ts +12 -0
  62. package/dist/services/DecoratorUtils.js +10 -0
  63. package/dist/services/EntityFactory.d.ts +25 -0
  64. package/dist/services/EntityFactory.js +27 -0
  65. package/dist/services/EntityRegistry.d.ts +61 -0
  66. package/dist/services/EntityRegistry.js +56 -0
  67. package/dist/services/EntitySchema.d.ts +56 -0
  68. package/dist/services/EntitySchema.js +125 -0
  69. package/dist/services/MigrationManager.d.ts +70 -0
  70. package/dist/services/MigrationManager.js +181 -0
  71. package/dist/services/RelationLoader.d.ts +66 -0
  72. package/dist/services/RelationLoader.js +310 -0
  73. package/dist/services/SchemaBuilder.d.ts +68 -0
  74. package/dist/services/SchemaBuilder.js +191 -0
  75. package/dist/services/index.d.ts +7 -0
  76. package/dist/services/index.js +7 -0
  77. package/dist/types.d.ts +152 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/logger.d.ts +12 -0
  80. package/dist/utils/logger.js +16 -0
  81. package/eslint.config.js +49 -0
  82. package/homepage/favicon.svg +36 -0
  83. package/homepage/index.html +1725 -0
  84. package/package.json +78 -0
  85. package/react-demo-app/README.md +61 -0
  86. package/react-demo-app/eslint.config.js +60 -0
  87. package/react-demo-app/index.html +13 -0
  88. package/react-demo-app/package-lock.json +4955 -0
  89. package/react-demo-app/package.json +39 -0
  90. package/react-demo-app/src/App.tsx +172 -0
  91. package/react-demo-app/src/assets/react.svg +1 -0
  92. package/react-demo-app/src/components/Actions.tsx +171 -0
  93. package/react-demo-app/src/components/CloudSyncDemo.tsx +191 -0
  94. package/react-demo-app/src/components/LiveQueryDemo.tsx +122 -0
  95. package/react-demo-app/src/components/MainInfo.tsx +75 -0
  96. package/react-demo-app/src/components/PostsLiveQueryDemo.tsx +185 -0
  97. package/react-demo-app/src/components/TypeScriptDemo.tsx +190 -0
  98. package/react-demo-app/src/database/Database.ts +30 -0
  99. package/react-demo-app/src/entities/Post.ts +48 -0
  100. package/react-demo-app/src/entities/PostTag.ts +26 -0
  101. package/react-demo-app/src/entities/Profile.ts +41 -0
  102. package/react-demo-app/src/entities/Tag.ts +35 -0
  103. package/react-demo-app/src/entities/User.ts +61 -0
  104. package/react-demo-app/src/hooks/useAppLogic.ts +565 -0
  105. package/react-demo-app/src/hooks/useCloudSyncDemo.ts +84 -0
  106. package/react-demo-app/src/hooks/useLiveQueryDemo.ts +68 -0
  107. package/react-demo-app/src/hooks/usePostsLiveQueryDemo.ts +64 -0
  108. package/react-demo-app/src/hooks/useTypeScriptDemo.ts +43 -0
  109. package/react-demo-app/src/index.css +26 -0
  110. package/react-demo-app/src/main.tsx +18 -0
  111. package/react-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
  112. package/react-demo-app/src/migrations/002-add-post-category.ts +37 -0
  113. package/react-demo-app/src/migrations/index.ts +8 -0
  114. package/react-demo-app/src/vite-env.d.ts +1 -0
  115. package/react-demo-app/tsconfig.app.json +22 -0
  116. package/react-demo-app/tsconfig.json +6 -0
  117. package/react-demo-app/vite.config.ts +10 -0
  118. package/src/Database.ts +405 -0
  119. package/src/errors/ValidationError.ts +9 -0
  120. package/src/index.ts +13 -0
  121. package/src/metadata/Column.ts +74 -0
  122. package/src/metadata/Entity.ts +42 -0
  123. package/src/metadata/Relation.ts +121 -0
  124. package/src/metadata/index.ts +5 -0
  125. package/src/services/AggregationService.ts +348 -0
  126. package/src/services/BaseEntity.ts +77 -0
  127. package/src/services/CloudSyncService.ts +248 -0
  128. package/src/services/EntityFactory.ts +35 -0
  129. package/src/services/EntityRegistry.ts +109 -0
  130. package/src/services/EntitySchema.ts +154 -0
  131. package/src/services/MigrationManager.ts +276 -0
  132. package/src/services/RelationLoader.ts +532 -0
  133. package/src/services/SchemaBuilder.ts +237 -0
  134. package/src/services/index.ts +7 -0
  135. package/src/types.d.ts +1 -0
  136. package/src/types.ts +169 -0
  137. package/src/utils/logger.ts +40 -0
  138. package/svelte-demo-app/README.md +61 -0
  139. package/svelte-demo-app/package-lock.json +3000 -0
  140. package/svelte-demo-app/package.json +30 -0
  141. package/svelte-demo-app/src/app.d.ts +12 -0
  142. package/svelte-demo-app/src/app.html +13 -0
  143. package/svelte-demo-app/src/components/Actions.svelte +121 -0
  144. package/svelte-demo-app/src/components/CloudSyncDemo.svelte +333 -0
  145. package/svelte-demo-app/src/components/LiveQueryDemo.svelte +191 -0
  146. package/svelte-demo-app/src/components/MainInfo.svelte +133 -0
  147. package/svelte-demo-app/src/components/PostsLiveQueryDemo.svelte +330 -0
  148. package/svelte-demo-app/src/components/TypeScriptDemo.svelte +251 -0
  149. package/svelte-demo-app/src/database/Database.ts +29 -0
  150. package/svelte-demo-app/src/entities/Post.ts +46 -0
  151. package/svelte-demo-app/src/entities/PostTag.ts +22 -0
  152. package/svelte-demo-app/src/entities/Profile.ts +39 -0
  153. package/svelte-demo-app/src/entities/Tag.ts +33 -0
  154. package/svelte-demo-app/src/entities/User.ts +62 -0
  155. package/svelte-demo-app/src/lib/database/Database.ts +30 -0
  156. package/svelte-demo-app/src/lib/entities/Post.ts +47 -0
  157. package/svelte-demo-app/src/lib/entities/PostTag.ts +23 -0
  158. package/svelte-demo-app/src/lib/entities/Profile.ts +40 -0
  159. package/svelte-demo-app/src/lib/entities/Tag.ts +34 -0
  160. package/svelte-demo-app/src/lib/entities/User.ts +59 -0
  161. package/svelte-demo-app/src/lib/index.ts +7 -0
  162. package/svelte-demo-app/src/lib/migrations/001-add-user-email-index.ts +17 -0
  163. package/svelte-demo-app/src/lib/migrations/002-add-post-category.ts +37 -0
  164. package/svelte-demo-app/src/lib/migrations/index.ts +8 -0
  165. package/svelte-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
  166. package/svelte-demo-app/src/migrations/002-add-post-category.ts +37 -0
  167. package/svelte-demo-app/src/migrations/index.ts +8 -0
  168. package/svelte-demo-app/src/routes/+layout.js +3 -0
  169. package/svelte-demo-app/src/routes/+layout.svelte +228 -0
  170. package/svelte-demo-app/src/routes/+page.js +3 -0
  171. package/svelte-demo-app/src/routes/+page.svelte +1305 -0
  172. package/svelte-demo-app/src/stores/appStore.js +603 -0
  173. package/svelte-demo-app/svelte.config.js +18 -0
  174. package/svelte-demo-app/tsconfig.json +14 -0
  175. package/svelte-demo-app/vite.config.ts +6 -0
  176. package/tests/aggregation.e2e.test.ts +87 -0
  177. package/tests/base-entity.e2e.test.ts +47 -0
  178. package/tests/database-api.e2e.test.ts +177 -0
  179. package/tests/decorators.e2e.test.ts +40 -0
  180. package/tests/entity-schema.e2e.test.ts +58 -0
  181. package/tests/relation-loader-table-names.test.ts +192 -0
  182. package/tests/relations.e2e.test.ts +178 -0
  183. package/tests/zod-runtime.e2e.test.ts +69 -0
  184. package/tsconfig.json +21 -0
  185. package/vitest.config.ts +21 -0
  186. package/vitest.setup.ts +27 -0
  187. package/vue-demo-app/README.md +61 -0
  188. package/vue-demo-app/index.html +13 -0
  189. package/vue-demo-app/package-lock.json +1537 -0
  190. package/vue-demo-app/package.json +27 -0
  191. package/vue-demo-app/src/App.vue +100 -0
  192. package/vue-demo-app/src/components/Actions.vue +135 -0
  193. package/vue-demo-app/src/components/CloudSyncDemo.vue +139 -0
  194. package/vue-demo-app/src/components/LiveQueryDemo.vue +122 -0
  195. package/vue-demo-app/src/components/MainInfo.vue +80 -0
  196. package/vue-demo-app/src/components/PostsLiveQueryDemo.vue +136 -0
  197. package/vue-demo-app/src/components/TypeScriptDemo.vue +133 -0
  198. package/vue-demo-app/src/database/Database.ts +29 -0
  199. package/vue-demo-app/src/entities/Post.ts +48 -0
  200. package/vue-demo-app/src/entities/PostTag.ts +24 -0
  201. package/vue-demo-app/src/entities/Profile.ts +41 -0
  202. package/vue-demo-app/src/entities/Tag.ts +35 -0
  203. package/vue-demo-app/src/entities/User.ts +61 -0
  204. package/vue-demo-app/src/main.ts +29 -0
  205. package/vue-demo-app/src/migrations/001-add-user-email-index.ts +23 -0
  206. package/vue-demo-app/src/migrations/002-add-post-category.ts +46 -0
  207. package/vue-demo-app/src/migrations/index.ts +14 -0
  208. package/vue-demo-app/src/services/useAppLogic.ts +565 -0
  209. package/vue-demo-app/src/services/useCloudSyncDemo.ts +84 -0
  210. package/vue-demo-app/src/services/useLiveQueryDemo.ts +82 -0
  211. package/vue-demo-app/src/services/usePostsLiveQueryDemo.ts +77 -0
  212. package/vue-demo-app/src/services/useTypeScriptDemo.ts +56 -0
  213. package/vue-demo-app/src/vite-env.d.ts +1 -0
  214. package/vue-demo-app/tsconfig.json +25 -0
  215. package/vue-demo-app/tsconfig.node.json +10 -0
  216. 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
+ }