@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,310 @@
1
+ import { getEntityMetadata } from '../metadata/Entity';
2
+ import { getRelationMetadata } from '../metadata/Relation';
3
+ export class RelationLoader {
4
+ db;
5
+ constructor(database) {
6
+ this.db = database;
7
+ }
8
+ resolveTableName(target) {
9
+ if (typeof target === 'string')
10
+ return target;
11
+ const meta = getEntityMetadata(target);
12
+ if (meta?.tableName)
13
+ return meta.tableName;
14
+ const n = target.name;
15
+ return (/Entity$/i.test(n)
16
+ ? n.replace(/Entity$/i, '')
17
+ : n).toLowerCase() + 's';
18
+ }
19
+ /**
20
+ * Load relations for an entity
21
+ * @param entity - The entity to load relations for
22
+ * @param entityClass - The entity class
23
+ * @param relationNames - Optional array of specific relation names to load.
24
+ * If not provided, loads all eager relations.
25
+ */
26
+ async loadRelations(entity, entityClass, relationNames) {
27
+ const relations = getRelationMetadata(entityClass);
28
+ if (!relations) {
29
+ return entity;
30
+ }
31
+ const loadedEntity = { ...entity };
32
+ const relationsToLoad = relationNames
33
+ ? relationNames.filter(name => relations[name])
34
+ : Object.entries(relations)
35
+ .filter(([, meta]) => meta.eager)
36
+ .map(([name]) => name);
37
+ for (const relationName of relationsToLoad) {
38
+ const relationMeta = relations[relationName];
39
+ if (relationMeta) {
40
+ loadedEntity[relationName] =
41
+ await this.loadRelation(entity, relationMeta);
42
+ }
43
+ }
44
+ return loadedEntity;
45
+ }
46
+ /**
47
+ * Load a specific relation by name
48
+ * @param entity - The entity to load relation for
49
+ * @param entityClass - The entity class
50
+ * @param relationName - The name of the relation to load
51
+ * @returns Promise resolving to the loaded relation data
52
+ */
53
+ async loadRelationByName(entity, entityClass, relationName) {
54
+ const relations = getRelationMetadata(entityClass);
55
+ if (!relations || !relations[String(relationName)]) {
56
+ throw new Error(`Relation
57
+ '${String(relationName)}' not found for entity ${entityClass.name}`);
58
+ }
59
+ const relationMeta = relations[String(relationName)];
60
+ return await this.loadRelation(entity, relationMeta);
61
+ }
62
+ /**
63
+ * Load a specific relation based on metadata
64
+ * @param entity - The entity to load relation for
65
+ * @param relationMeta - The relation metadata containing:
66
+ * type, target, and configuration
67
+ * @returns Promise resolving to the loaded relation data
68
+ */
69
+ async loadRelation(entity, relationMeta) {
70
+ const targetName = this.resolveTableName(relationMeta.target);
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ const targetTable = this.db.table(targetName);
73
+ switch (relationMeta.type) {
74
+ case 'one-to-one':
75
+ return await this.loadOneToOne(entity, targetTable, relationMeta);
76
+ case 'one-to-many':
77
+ return await this.loadOneToMany(entity, targetTable, relationMeta);
78
+ case 'many-to-many':
79
+ return await this.loadManyToMany(entity, targetTable, relationMeta);
80
+ default:
81
+ throw new Error(`Unsupported relation type: ${relationMeta.type}`);
82
+ }
83
+ }
84
+ async loadOneToOne(entity, targetTable, relationMeta) {
85
+ if (relationMeta.foreignKey) {
86
+ return await targetTable
87
+ .where(relationMeta.foreignKey)
88
+ .equals(entity.id)
89
+ .first();
90
+ }
91
+ else {
92
+ const foreignKeyValue = entity[relationMeta.foreignKey];
93
+ if (!foreignKeyValue) {
94
+ return null;
95
+ }
96
+ return await targetTable.get(foreignKeyValue);
97
+ }
98
+ }
99
+ async loadOneToMany(entity, targetTable, relationMeta) {
100
+ if (!relationMeta.foreignKey) {
101
+ return [];
102
+ }
103
+ return await targetTable
104
+ .where(relationMeta.foreignKey)
105
+ .equals(entity.id)
106
+ .toArray();
107
+ }
108
+ /**
109
+ * Load many-to-many relation
110
+ */
111
+ async loadManyToMany(entity, targetTable, relationMeta) {
112
+ if (!relationMeta.joinTable) {
113
+ return [];
114
+ }
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ const joinTable = this.db.table(relationMeta.joinTable);
117
+ const joinRecords = await joinTable
118
+ .where('sourceId')
119
+ .equals(entity.id)
120
+ .toArray();
121
+ const targetIds = joinRecords.map((record) => record.targetId);
122
+ if (targetIds.length === 0) {
123
+ return [];
124
+ }
125
+ return await targetTable
126
+ .where('id')
127
+ .anyOf(targetIds)
128
+ .toArray();
129
+ }
130
+ /**
131
+ * Save entity with all its relations
132
+ * @param entity - The entity to save with relations
133
+ * @param entityClass - The entity class
134
+ * @returns Promise resolving to the saved entity with relations
135
+ */
136
+ async saveWithRelations(entity, entityClass) {
137
+ const relations = getRelationMetadata(entityClass);
138
+ if (!relations) {
139
+ return await this.saveEntity(entity, entityClass);
140
+ }
141
+ const savedEntity = await this.saveEntity(entity, entityClass);
142
+ for (const [propertyKey, relationMeta] of Object.entries(relations)) {
143
+ const relationData = entity[propertyKey];
144
+ if (relationData) {
145
+ await this.saveRelation(savedEntity, relationData, relationMeta);
146
+ }
147
+ }
148
+ return savedEntity;
149
+ }
150
+ /**
151
+ * Delete entity with cascade handling for relations
152
+ * @param entity - The entity to delete
153
+ * @param entityClass - The entity class
154
+ */
155
+ async deleteWithRelations(entity, entityClass) {
156
+ const relations = getRelationMetadata(entityClass);
157
+ if (relations) {
158
+ for (const [, relationMeta] of Object.entries(relations)) {
159
+ if (!relationMeta.cascade) {
160
+ continue;
161
+ }
162
+ await this.deleteRelation(entity, relationMeta);
163
+ }
164
+ }
165
+ const baseMeta = getEntityMetadata(entityClass);
166
+ const baseName = baseMeta?.tableName
167
+ || ((/Entity$/i.test(entityClass.name)
168
+ ? entityClass.name.replace(/Entity$/i, '')
169
+ : entityClass.name).toLowerCase() + 's');
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ const table = this.db.table(baseName);
172
+ await table.delete(entity.id);
173
+ }
174
+ async deleteRelation(entity, relationMeta) {
175
+ const targetName = this.resolveTableName(relationMeta.target);
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const targetTable = this.db.table(targetName);
178
+ switch (relationMeta.type) {
179
+ case 'one-to-one':
180
+ await this.deleteOneToOne(entity, targetTable, relationMeta);
181
+ break;
182
+ case 'one-to-many':
183
+ await this.deleteOneToMany(entity, targetTable, relationMeta);
184
+ break;
185
+ case 'many-to-many':
186
+ await this.deleteManyToMany(entity, relationMeta);
187
+ break;
188
+ }
189
+ }
190
+ async deleteOneToOne(entity, targetTable, relationMeta) {
191
+ if (relationMeta.foreignKey) {
192
+ await targetTable
193
+ .where(relationMeta.foreignKey)
194
+ .equals(entity.id)
195
+ .delete();
196
+ }
197
+ else if (entity.id) {
198
+ const related = await targetTable.get(entity.id);
199
+ if (related) {
200
+ await targetTable.delete(entity.id);
201
+ }
202
+ }
203
+ }
204
+ async deleteOneToMany(entity, targetTable, relationMeta) {
205
+ if (!relationMeta.foreignKey) {
206
+ return;
207
+ }
208
+ await targetTable
209
+ .where(relationMeta.foreignKey)
210
+ .equals(entity.id)
211
+ .delete();
212
+ }
213
+ async deleteManyToMany(entity, relationMeta) {
214
+ if (!relationMeta.joinTable) {
215
+ return;
216
+ }
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ const joinTable = this.db.table(relationMeta.joinTable);
219
+ await joinTable
220
+ .where('sourceId')
221
+ .equals(entity.id)
222
+ .delete();
223
+ }
224
+ async saveEntity(entity, entityClass) {
225
+ const baseMeta2 = getEntityMetadata(entityClass);
226
+ const baseName2 = baseMeta2?.tableName
227
+ || ((/Entity$/i.test(entityClass.name)
228
+ ? entityClass.name.replace(/Entity$/i, '')
229
+ : entityClass.name).toLowerCase() + 's');
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
231
+ const table = this.db.table(baseName2);
232
+ if (entity.id) {
233
+ await table.put(entity);
234
+ return entity;
235
+ }
236
+ else {
237
+ const id = await table.add(entity);
238
+ return { ...entity, id };
239
+ }
240
+ }
241
+ async saveRelation(entity, relationData, relationMeta) {
242
+ const targetName = this.resolveTableName(relationMeta.target);
243
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
+ const targetTable = this.db.table(targetName);
245
+ switch (relationMeta.type) {
246
+ case 'one-to-one':
247
+ await this.saveOneToOne(entity, relationData, targetTable, relationMeta);
248
+ break;
249
+ case 'one-to-many':
250
+ await this
251
+ .saveOneToMany(entity, relationData, targetTable, relationMeta);
252
+ break;
253
+ case 'many-to-many':
254
+ await this.saveManyToMany(entity, relationData, targetTable, relationMeta);
255
+ break;
256
+ }
257
+ }
258
+ async saveOneToOne(entity, relationData, targetTable, relationMeta) {
259
+ const data = relationData;
260
+ if (relationMeta.foreignKey) {
261
+ data[relationMeta.foreignKey] = entity.id;
262
+ }
263
+ if (data.id) {
264
+ await targetTable.update(data.id, data);
265
+ }
266
+ else {
267
+ await targetTable.add(data);
268
+ }
269
+ }
270
+ async saveOneToMany(entity, relationData, targetTable, relationMeta) {
271
+ if (!relationMeta.foreignKey) {
272
+ return;
273
+ }
274
+ for (const item of relationData) {
275
+ const data = item;
276
+ data[relationMeta.foreignKey] = entity.id;
277
+ if (data.id) {
278
+ await targetTable.update(data.id, data);
279
+ }
280
+ else {
281
+ await targetTable.add(data);
282
+ }
283
+ }
284
+ }
285
+ async saveManyToMany(entity, relationData, targetTable, relationMeta) {
286
+ if (!relationMeta.joinTable) {
287
+ return;
288
+ }
289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
+ const joinTable = this.db.table(relationMeta.joinTable);
291
+ await joinTable
292
+ .where('sourceId')
293
+ .equals(entity.id)
294
+ .delete();
295
+ for (const item of relationData) {
296
+ const data = item;
297
+ let targetId = data.id;
298
+ if (!targetId) {
299
+ targetId = await targetTable.add(data);
300
+ }
301
+ else {
302
+ await targetTable.update(data.id, data);
303
+ }
304
+ await joinTable.add({
305
+ sourceId: entity.id,
306
+ targetId: targetId,
307
+ });
308
+ }
309
+ }
310
+ }
@@ -0,0 +1,68 @@
1
+ import type { EntityConstructor } from '../types';
2
+ export interface TableSchema {
3
+ tableName: string;
4
+ schema: string;
5
+ }
6
+ export interface SchemaChange {
7
+ tableName: string;
8
+ changeType: 'added' | 'removed' | 'modified';
9
+ oldSchema?: string;
10
+ newSchema?: string;
11
+ }
12
+ export declare class SchemaBuilder {
13
+ /**
14
+ * Build indexeddb schema from entities
15
+ * @param entities - Array of entity constructors
16
+ * @returns IndexedDB schema object with table definitions
17
+ *
18
+ * @example
19
+ * const schema = SchemaBuilder.buildSchema([User, Post]);
20
+ */
21
+ static buildSchema(entities: EntityConstructor[]): Record<string, string>;
22
+ /**
23
+ * Build detailed table schemas for comparison
24
+ * @param entities - Array of entity constructors
25
+ * @returns Array of detailed table schema objects
26
+ *
27
+ * @example
28
+ * const tables = SchemaBuilder.buildTableSchemas([User, Post]);
29
+ */
30
+ static buildTableSchemas(entities: EntityConstructor[]): TableSchema[];
31
+ /**
32
+ * Generate hash for schema to detect changes
33
+ * @param schema - Schema object to hash
34
+ * @returns Hash string for schema comparison
35
+ *
36
+ * @example
37
+ * const hash = SchemaBuilder.generateSchemaHash(schema);
38
+ */
39
+ static generateSchemaHash(schema: Record<string, string>): string;
40
+ /**
41
+ * Compare two schema arrays and return changes
42
+ * @param oldSchemas - Previous schema array
43
+ * @param newSchemas - Current schema array
44
+ * @returns Array of schema changes detected
45
+ *
46
+ * @example
47
+ * const changes = SchemaBuilder.compareSchemas(oldSchemas, newSchemas);
48
+ */
49
+ static compareSchemas(oldSchemas: TableSchema[], newSchemas: TableSchema[]): SchemaChange[];
50
+ /**
51
+ * Store table schemas in localStorage
52
+ * @param dbName - Database name for key generation
53
+ * @param tableSchemas - Array of table schemas to store
54
+ *
55
+ * @example
56
+ * SchemaBuilder.storeTableSchemas('app-db', tables);
57
+ */
58
+ static storeTableSchemas(dbName: string, tableSchemas: TableSchema[]): void;
59
+ /**
60
+ * Get stored table schemas from localStorage
61
+ * @param dbName - Database name for key lookup
62
+ * @returns Array of stored table schemas or empty array if none found
63
+ *
64
+ * @example
65
+ * const tables = SchemaBuilder.getStoredTableSchemas('app-db');
66
+ */
67
+ static getStoredTableSchemas(dbName: string): TableSchema[];
68
+ }
@@ -0,0 +1,191 @@
1
+ import { getColumnMetadata, getCompoundIndexMetadata, } from '../metadata/Column';
2
+ import { getEntityMetadata } from '../metadata/Entity';
3
+ import { logger } from '../utils/logger';
4
+ import { getDefinedCompoundIndexes } from './EntityRegistry';
5
+ export class SchemaBuilder {
6
+ /**
7
+ * Build indexeddb schema from entities
8
+ * @param entities - Array of entity constructors
9
+ * @returns IndexedDB schema object with table definitions
10
+ *
11
+ * @example
12
+ * const schema = SchemaBuilder.buildSchema([User, Post]);
13
+ */
14
+ static buildSchema(entities) {
15
+ const schema = {};
16
+ schema.migrationMetadata = 'key, value, updatedAt';
17
+ entities.forEach(entity => {
18
+ const metadata = getEntityMetadata(entity);
19
+ const tableName = metadata?.tableName || entity.name.toLowerCase() + 's';
20
+ const columns = getColumnMetadata(entity);
21
+ const compoundIndexes = getDefinedCompoundIndexes(entity)
22
+ || getCompoundIndexMetadata(entity);
23
+ let primaryKeySpec = '';
24
+ const indexSpecs = [];
25
+ Object.entries(columns).forEach(([propertyKey, columnMeta]) => {
26
+ if (columnMeta.primaryKey) {
27
+ primaryKeySpec = columnMeta.autoIncrement
28
+ ? `++${propertyKey}`
29
+ : propertyKey;
30
+ }
31
+ else if (columnMeta.unique) {
32
+ indexSpecs.push(`&${propertyKey}`);
33
+ }
34
+ else if (columnMeta.indexed) {
35
+ indexSpecs.push(`${propertyKey}`);
36
+ }
37
+ });
38
+ const compoundIndexStrings = [];
39
+ compoundIndexes.forEach(compoundIndex => {
40
+ const indexColumns = compoundIndex.columns.join('+');
41
+ const bracketed = `[${indexColumns}]`;
42
+ const spec = compoundIndex.unique ? `&${bracketed}` : `${bracketed}`;
43
+ compoundIndexStrings.push(spec);
44
+ });
45
+ const parts = [
46
+ primaryKeySpec, ...indexSpecs, ...compoundIndexStrings,
47
+ ].filter(Boolean);
48
+ schema[tableName] = parts.join(',');
49
+ if (!schema[tableName]) {
50
+ schema[tableName] = '++id';
51
+ }
52
+ });
53
+ return schema;
54
+ }
55
+ /**
56
+ * Build detailed table schemas for comparison
57
+ * @param entities - Array of entity constructors
58
+ * @returns Array of detailed table schema objects
59
+ *
60
+ * @example
61
+ * const tables = SchemaBuilder.buildTableSchemas([User, Post]);
62
+ */
63
+ static buildTableSchemas(entities) {
64
+ return entities.map(entity => {
65
+ const metadata = getEntityMetadata(entity);
66
+ const tableName = metadata?.tableName || entity.name.toLowerCase() + 's';
67
+ const columns = getColumnMetadata(entity);
68
+ const compoundIndexes = getCompoundIndexMetadata(entity);
69
+ let primaryKeySpec = '';
70
+ const indexSpecs = [];
71
+ Object.entries(columns).forEach(([propertyKey, columnMeta]) => {
72
+ if (columnMeta.primaryKey) {
73
+ primaryKeySpec = columnMeta.autoIncrement
74
+ ? `++${propertyKey}`
75
+ : propertyKey;
76
+ }
77
+ else if (columnMeta.unique) {
78
+ indexSpecs.push(`&${propertyKey}`);
79
+ }
80
+ else if (columnMeta.indexed) {
81
+ indexSpecs.push(`${propertyKey}`);
82
+ }
83
+ });
84
+ const compoundIndexStrings = [];
85
+ compoundIndexes.forEach(compoundIndex => {
86
+ const indexColumns = compoundIndex.columns.join('+');
87
+ const bracketed = `[${indexColumns}]`;
88
+ const spec = compoundIndex.unique ? `&${bracketed}` : `${bracketed}`;
89
+ compoundIndexStrings.push(spec);
90
+ });
91
+ const parts = [
92
+ primaryKeySpec, ...indexSpecs, ...compoundIndexStrings,
93
+ ].filter(Boolean);
94
+ return {
95
+ tableName,
96
+ schema: parts.join(','),
97
+ };
98
+ });
99
+ }
100
+ /**
101
+ * Generate hash for schema to detect changes
102
+ * @param schema - Schema object to hash
103
+ * @returns Hash string for schema comparison
104
+ *
105
+ * @example
106
+ * const hash = SchemaBuilder.generateSchemaHash(schema);
107
+ */
108
+ static generateSchemaHash(schema) {
109
+ const schemaString = JSON.stringify(schema, Object.keys(schema).sort());
110
+ return btoa(schemaString).slice(0, 16);
111
+ }
112
+ /**
113
+ * Compare two schema arrays and return changes
114
+ * @param oldSchemas - Previous schema array
115
+ * @param newSchemas - Current schema array
116
+ * @returns Array of schema changes detected
117
+ *
118
+ * @example
119
+ * const changes = SchemaBuilder.compareSchemas(oldSchemas, newSchemas);
120
+ */
121
+ static compareSchemas(oldSchemas, newSchemas) {
122
+ const changes = [];
123
+ const oldSchemaMap = new Map(oldSchemas.map(s => [s.tableName, s]));
124
+ const newSchemaMap = new Map(newSchemas.map(s => [s.tableName, s]));
125
+ newSchemas.forEach(newSchema => {
126
+ if (!oldSchemaMap.has(newSchema.tableName)) {
127
+ changes.push({
128
+ tableName: newSchema.tableName,
129
+ changeType: 'added',
130
+ newSchema: newSchema.schema,
131
+ });
132
+ }
133
+ });
134
+ oldSchemas.forEach(oldSchema => {
135
+ if (!newSchemaMap.has(oldSchema.tableName)) {
136
+ changes.push({
137
+ tableName: oldSchema.tableName,
138
+ changeType: 'removed',
139
+ oldSchema: oldSchema.schema,
140
+ });
141
+ }
142
+ });
143
+ newSchemas.forEach(newSchema => {
144
+ const oldSchema = oldSchemaMap.get(newSchema.tableName);
145
+ if (oldSchema && oldSchema.schema !== newSchema.schema) {
146
+ changes.push({
147
+ tableName: newSchema.tableName,
148
+ changeType: 'modified',
149
+ oldSchema: oldSchema.schema,
150
+ newSchema: newSchema.schema,
151
+ });
152
+ }
153
+ });
154
+ return changes;
155
+ }
156
+ /**
157
+ * Store table schemas in localStorage
158
+ * @param dbName - Database name for key generation
159
+ * @param tableSchemas - Array of table schemas to store
160
+ *
161
+ * @example
162
+ * SchemaBuilder.storeTableSchemas('app-db', tables);
163
+ */
164
+ static storeTableSchemas(dbName, tableSchemas) {
165
+ const key = `${dbName}_table_schemas`;
166
+ const serialized = JSON.stringify(tableSchemas);
167
+ localStorage.setItem(key, serialized);
168
+ }
169
+ /**
170
+ * Get stored table schemas from localStorage
171
+ * @param dbName - Database name for key lookup
172
+ * @returns Array of stored table schemas or empty array if none found
173
+ *
174
+ * @example
175
+ * const tables = SchemaBuilder.getStoredTableSchemas('app-db');
176
+ */
177
+ static getStoredTableSchemas(dbName) {
178
+ const key = `${dbName}_table_schemas`;
179
+ const stored = localStorage.getItem(key);
180
+ if (!stored) {
181
+ return [];
182
+ }
183
+ try {
184
+ return JSON.parse(stored);
185
+ }
186
+ catch (error) {
187
+ logger.error('Failed to parse stored table schemas:', error);
188
+ return [];
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,7 @@
1
+ export { AggregationService } from './AggregationService';
2
+ export { BaseEntity } from './BaseEntity';
3
+ export { CloudSyncService } from './CloudSyncService';
4
+ export { EntitySchema } from './EntitySchema';
5
+ export { MigrationManager } from './MigrationManager';
6
+ export { RelationLoader } from './RelationLoader';
7
+ export { SchemaBuilder } from './SchemaBuilder';
@@ -0,0 +1,7 @@
1
+ export { AggregationService } from './AggregationService';
2
+ export { BaseEntity } from './BaseEntity';
3
+ export { CloudSyncService } from './CloudSyncService';
4
+ export { EntitySchema } from './EntitySchema';
5
+ export { MigrationManager } from './MigrationManager';
6
+ export { RelationLoader } from './RelationLoader';
7
+ export { SchemaBuilder } from './SchemaBuilder';