@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,229 @@
1
+ import { getRelationMetadata } from '../metadata/Relation';
2
+ export class AggregationService {
3
+ db;
4
+ constructor(database) {
5
+ this.db = database;
6
+ }
7
+ /**
8
+ * Perform aggregation operations on entity data
9
+ * @param entityClass - The entity class to aggregate
10
+ * @param options - Aggregation options including filters, sorting, grouping, etc.
11
+ * @returns Promise resolving to aggregation results
12
+ *
13
+ * @example
14
+ * const result = await aggregationService.aggregate(Post, {
15
+ * where: { category: 'tech' },
16
+ * sort: { field: 'views', direction: 'desc' },
17
+ * limit: 10,
18
+ * count: true,
19
+ * sum: ['views'],
20
+ * });
21
+ */
22
+ async aggregate(entityClass, options) {
23
+ const table = this.db.getRepository(entityClass);
24
+ let collection = table.toCollection();
25
+ if (options.where) {
26
+ const whereConditions = options.where;
27
+ collection = collection.filter((record) => {
28
+ return Object.entries(whereConditions).every(([key, value]) => {
29
+ return record[key] === value;
30
+ });
31
+ });
32
+ }
33
+ let records = await collection.toArray();
34
+ if (options.include && options.include.length > 0) {
35
+ const relations = getRelationMetadata(entityClass);
36
+ if (relations) {
37
+ for (const record of records) {
38
+ for (const relationName of options.include) {
39
+ if (relations[relationName]) {
40
+ record[relationName] =
41
+ await this.loadRelation(record, relations[relationName]);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ if (options.sort) {
48
+ const sortField = options.sort.field;
49
+ const sortDirection = options.sort.direction;
50
+ records.sort((a, b) => {
51
+ const aVal = a[sortField];
52
+ const bVal = b[sortField];
53
+ const direction = sortDirection === 'asc' ? 1 : -1;
54
+ if (aVal < bVal) {
55
+ return -1 * direction;
56
+ }
57
+ if (aVal > bVal) {
58
+ return 1 * direction;
59
+ }
60
+ return 0;
61
+ });
62
+ }
63
+ if (options.limit) {
64
+ records = records.slice(0, options.limit);
65
+ }
66
+ const result = {};
67
+ if (options.count) {
68
+ result.count = records.length;
69
+ }
70
+ if (options.sum || options.avg || options.min || options.max) {
71
+ const numericFields = [
72
+ ...(options.sum || []),
73
+ ...(options.avg || []),
74
+ ...(options.min || []),
75
+ ...(options.max || []),
76
+ ];
77
+ for (const field of numericFields) {
78
+ const values = records
79
+ .map((r) => r[field])
80
+ .filter((v) => typeof v === 'number' && !isNaN(v));
81
+ if (values.length === 0) {
82
+ continue;
83
+ }
84
+ if (options.sum?.includes(field)) {
85
+ result.sum = result.sum || {};
86
+ result.sum[field] = values.reduce((sum, val) => sum + val, 0);
87
+ }
88
+ if (options.avg?.includes(field)) {
89
+ result.avg = result.avg || {};
90
+ result.avg[field] = values
91
+ .reduce((sum, val) => sum + val, 0) / values.length;
92
+ }
93
+ if (options.min?.includes(field)) {
94
+ result.min = result.min || {};
95
+ result.min[field] = Math.min(...values);
96
+ }
97
+ if (options.max?.includes(field)) {
98
+ result.max = result.max || {};
99
+ result.max[field] = Math.max(...values);
100
+ }
101
+ }
102
+ }
103
+ if (options.groupBy) {
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ const groups = new Map();
106
+ for (const record of records) {
107
+ const key = record[options.groupBy];
108
+ if (!groups.has(key)) {
109
+ groups.set(key, []);
110
+ }
111
+ const group = groups.get(key);
112
+ if (group) {
113
+ group.push(record);
114
+ }
115
+ }
116
+ result.groups = Array.from(groups.entries()).map(([key, groupRecords]) => {
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ const groupResult = { key, count: groupRecords.length };
119
+ if (options.sum || options.avg || options.min || options.max) {
120
+ const numericFields = [
121
+ ...(options.sum || []),
122
+ ...(options.avg || []),
123
+ ...(options.min || []),
124
+ ...(options.max || []),
125
+ ];
126
+ for (const field of numericFields) {
127
+ const values = groupRecords
128
+ .map(r => r[field])
129
+ .filter(v => typeof v === 'number' && !isNaN(v));
130
+ if (values.length === 0) {
131
+ continue;
132
+ }
133
+ if (options.sum?.includes(field)) {
134
+ groupResult.sum = groupResult.sum || {};
135
+ groupResult.sum[field] = values
136
+ .reduce((sum, val) => sum + val, 0);
137
+ }
138
+ if (options.avg?.includes(field)) {
139
+ groupResult.avg = groupResult.avg || {};
140
+ groupResult.avg[field] = values.reduce((sum, val) => sum + val, 0) / values.length;
141
+ }
142
+ if (options.min?.includes(field)) {
143
+ groupResult.min = groupResult.min || {};
144
+ groupResult.min[field] = Math.min(...values);
145
+ }
146
+ if (options.max?.includes(field)) {
147
+ groupResult.max = groupResult.max || {};
148
+ groupResult.max[field] = Math.max(...values);
149
+ }
150
+ }
151
+ }
152
+ return groupResult;
153
+ });
154
+ }
155
+ if (!options.count &&
156
+ !options.sum &&
157
+ !options.avg &&
158
+ !options.min &&
159
+ !options.max &&
160
+ !options.groupBy) {
161
+ result.data = records;
162
+ }
163
+ return result;
164
+ }
165
+ /**
166
+ * Load a specific relation based on metadata.
167
+ * @param entity - The source entity to load relation for
168
+ * @param relationMeta - Relation metadata (type, target, keys)
169
+ * @returns Promise resolving to the loaded relation data
170
+ *
171
+ * @example
172
+ * const related = await (aggregationService as any).loadRelation(record, {
173
+ * type: 'one-to-many', target: Post, foreignKey: 'authorId',
174
+ * });
175
+ */
176
+ async loadRelation(entity, relationMeta) {
177
+ const targetTable = this.db
178
+ .getRepository(relationMeta.target);
179
+ switch (relationMeta.type) {
180
+ case 'one-to-one':
181
+ return await this.loadOneToOne(entity, targetTable, relationMeta);
182
+ case 'one-to-many':
183
+ return await this.loadOneToMany(entity, targetTable, relationMeta);
184
+ case 'many-to-many':
185
+ return await this.loadManyToMany(entity, targetTable, relationMeta);
186
+ default:
187
+ throw new Error(`Unsupported relation type: ${relationMeta.type}`);
188
+ }
189
+ }
190
+ async loadOneToOne(entity, _targetTable, relationMeta) {
191
+ const targetTableInstance = this.db.getRepository(relationMeta.target);
192
+ if (relationMeta.foreignKey) {
193
+ return await targetTableInstance
194
+ .where(relationMeta.foreignKey)
195
+ .equals(entity.id)
196
+ .first();
197
+ }
198
+ else {
199
+ const foreignKeyValue = relationMeta.foreignKey
200
+ ? entity[relationMeta.foreignKey] : undefined;
201
+ if (!foreignKeyValue)
202
+ return null;
203
+ return await targetTableInstance.get(foreignKeyValue);
204
+ }
205
+ }
206
+ async loadOneToMany(entity, _targetTable, relationMeta) {
207
+ const targetTableInstance = this.db.getRepository(relationMeta.target);
208
+ return await targetTableInstance
209
+ .where(relationMeta.foreignKey)
210
+ .equals(entity.id)
211
+ .toArray();
212
+ }
213
+ async loadManyToMany(entity, _targetTable, relationMeta) {
214
+ const joinTable = this.db.table(relationMeta.joinTable);
215
+ const joinRecords = await joinTable
216
+ .where('sourceId')
217
+ .equals(entity.id)
218
+ .toArray();
219
+ const targetIds = joinRecords.map((record) => record.targetId);
220
+ if (targetIds.length === 0) {
221
+ return [];
222
+ }
223
+ const targetTableInstance = this.db.getRepository(relationMeta.target);
224
+ return await targetTableInstance
225
+ .where('id')
226
+ .anyOf(targetIds)
227
+ .toArray();
228
+ }
229
+ }
@@ -0,0 +1,32 @@
1
+ import type { ValidationResult } from '../types';
2
+ export declare abstract class BaseEntity<TKey extends string | number = number | string> {
3
+ id?: TKey;
4
+ constructor(data?: Partial<BaseEntity<TKey>>);
5
+ /**
6
+ * Validate entity data against its schema
7
+ * @returns ValidationResult containing validation status and errors
8
+ *
9
+ * @example
10
+ * const result = user.validate();
11
+ * if (!result.isValid) console.error(result.errors);
12
+ */
13
+ validate(): ValidationResult;
14
+ /**
15
+ * Validate entity data and throw error if invalid
16
+ * @throws ValidationError if validation fails
17
+ *
18
+ * @example
19
+ * user.validateOrThrow(); // throws ValidationError on invalid data
20
+ */
21
+ validateOrThrow(): void;
22
+ /**
23
+ * Initialize entity with data and validate
24
+ * @param data - Partial data to initialize entity with
25
+ * @returns The initialized entity instance
26
+ * @throws ValidationError if validation fails
27
+ *
28
+ * @example
29
+ * const user = new User().init({ name: 'Alice' });
30
+ */
31
+ init(data: Partial<this>): this;
32
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import { ValidationError } from '../errors/ValidationError';
3
+ export class BaseEntity {
4
+ id;
5
+ constructor(data) {
6
+ if (data) {
7
+ Object.assign(this, data);
8
+ }
9
+ }
10
+ /**
11
+ * Validate entity data against its schema
12
+ * @returns ValidationResult containing validation status and errors
13
+ *
14
+ * @example
15
+ * const result = user.validate();
16
+ * if (!result.isValid) console.error(result.errors);
17
+ */
18
+ validate() {
19
+ const constructor = this.constructor;
20
+ if (!constructor.schema) {
21
+ return { isValid: true, errors: [] };
22
+ }
23
+ try {
24
+ constructor.schema.parse(this);
25
+ return { isValid: true, errors: [] };
26
+ }
27
+ catch (error) {
28
+ if (error instanceof z.ZodError) {
29
+ const errors = error.issues.map(err => `${err.path.join('.')}: ${err.message}`);
30
+ return { isValid: false, errors };
31
+ }
32
+ return { isValid: false, errors: ['Unknown validation error'] };
33
+ }
34
+ }
35
+ /**
36
+ * Validate entity data and throw error if invalid
37
+ * @throws ValidationError if validation fails
38
+ *
39
+ * @example
40
+ * user.validateOrThrow(); // throws ValidationError on invalid data
41
+ */
42
+ validateOrThrow() {
43
+ const result = this.validate();
44
+ if (!result.isValid) {
45
+ throw new ValidationError('Entity validation failed', result.errors);
46
+ }
47
+ }
48
+ /**
49
+ * Initialize entity with data and validate
50
+ * @param data - Partial data to initialize entity with
51
+ * @returns The initialized entity instance
52
+ * @throws ValidationError if validation fails
53
+ *
54
+ * @example
55
+ * const user = new User().init({ name: 'Alice' });
56
+ */
57
+ init(data) {
58
+ Object.assign(this, data);
59
+ this.validateOrThrow();
60
+ return this;
61
+ }
62
+ }
@@ -0,0 +1,100 @@
1
+ import type { Database } from '../Database';
2
+ import type { CloudSyncConfig } from '../types';
3
+ export declare class CloudSyncService {
4
+ private db;
5
+ private cloudSyncConfig?;
6
+ private cloudSyncEnabled;
7
+ private syncIntervalId?;
8
+ cloud: {
9
+ sync: () => Promise<void>;
10
+ lastSync?: number;
11
+ isOnline?: boolean;
12
+ };
13
+ constructor(database: Database);
14
+ /**
15
+ * Initialize cloud synchronization with configuration
16
+ * @param config - Cloud sync configuration
17
+ * @returns Promise that resolves when initialization is complete
18
+ *
19
+ * @example
20
+ * await cloudService.initializeCloudSync({
21
+ * databaseUrl: 'https://example.cloud',
22
+ * enableOfflineSupport: true,
23
+ * syncInterval: 30000,
24
+ * });
25
+ */
26
+ initializeCloudSync(config: CloudSyncConfig): Promise<void>;
27
+ private startPeriodicSync;
28
+ /**
29
+ * Stop periodic synchronization
30
+ *
31
+ * @example
32
+ * cloudService.stopPeriodicSync();
33
+ */
34
+ stopPeriodicSync(): void;
35
+ /**
36
+ * Perform manual synchronization
37
+ * @returns Promise that resolves when sync is complete
38
+ * @throws Error if cloud sync is not enabled
39
+ *
40
+ * @example
41
+ * await cloudService.sync();
42
+ */
43
+ sync(): Promise<void>;
44
+ /**
45
+ * Get current synchronization status
46
+ * @returns Object containing sync status information
47
+ *
48
+ * @example
49
+ * const { enabled, lastSync, isOnline } = cloudService.getSyncStatus();
50
+ */
51
+ getSyncStatus(): {
52
+ enabled: boolean;
53
+ lastSync?: Date;
54
+ isOnline?: boolean;
55
+ };
56
+ /**
57
+ * Enable cloud synchronization
58
+ * @param config - Cloud sync configuration
59
+ * @returns Promise that resolves when cloud sync is enabled
60
+ *
61
+ * @example
62
+ * await cloudService.enableCloudSync({ databaseUrl: 'https://example.cloud' });
63
+ */
64
+ enableCloudSync(config: CloudSyncConfig): Promise<void>;
65
+ /**
66
+ * Disable cloud synchronization
67
+ *
68
+ * @example
69
+ * cloudService.disableCloudSync();
70
+ */
71
+ disableCloudSync(): void;
72
+ /**
73
+ * Check if cloud sync is currently enabled
74
+ * @returns True if cloud sync is enabled, false otherwise
75
+ *
76
+ * @example
77
+ * if (cloudService.isCloudSyncEnabled()) {
78
+ * // ...
79
+ * }
80
+ */
81
+ isCloudSyncEnabled(): boolean;
82
+ /**
83
+ * Get current cloud sync configuration
84
+ * @returns Cloud sync configuration or undefined if not set
85
+ *
86
+ * @example
87
+ * const cfg = cloudService.getCloudSyncConfig();
88
+ */
89
+ getCloudSyncConfig(): CloudSyncConfig | undefined;
90
+ /**
91
+ * Synchronize specific tables
92
+ * @param tableNames - Array of table names to sync
93
+ * @returns Promise that resolves when sync is complete
94
+ * @throws Error if cloud sync is not enabled
95
+ *
96
+ * @example
97
+ * await cloudService.syncTables(['users', 'posts']);
98
+ */
99
+ syncTables(tableNames: string[]): Promise<void>;
100
+ }
@@ -0,0 +1,196 @@
1
+ import { createServiceLogger } from '../utils/logger';
2
+ const logger = createServiceLogger('CloudSyncService');
3
+ export class CloudSyncService {
4
+ db;
5
+ cloudSyncConfig;
6
+ cloudSyncEnabled = false;
7
+ syncIntervalId;
8
+ constructor(database) {
9
+ this.db = database;
10
+ }
11
+ /**
12
+ * Initialize cloud synchronization with configuration
13
+ * @param config - Cloud sync configuration
14
+ * @returns Promise that resolves when initialization is complete
15
+ *
16
+ * @example
17
+ * await cloudService.initializeCloudSync({
18
+ * databaseUrl: 'https://example.cloud',
19
+ * enableOfflineSupport: true,
20
+ * syncInterval: 30000,
21
+ * });
22
+ */
23
+ async initializeCloudSync(config) {
24
+ this.cloudSyncConfig = config;
25
+ if (!this.cloudSyncConfig) {
26
+ logger.error('Cloud sync config not provided');
27
+ return;
28
+ }
29
+ try {
30
+ const cloudApi = this.db
31
+ .cloud;
32
+ if (!cloudApi) {
33
+ logger.error('Dexie cloud addon not detected. Initialize Dexie with dexie-cloud-addon: new Dexie(name, { addons: [dexieCloud] })');
34
+ return;
35
+ }
36
+ logger.info('Initializing cloud sync...');
37
+ cloudApi.configure({
38
+ databaseUrl: this.cloudSyncConfig.databaseUrl,
39
+ enableOfflineSupport: this.cloudSyncConfig.enableOfflineSupport ?? true,
40
+ });
41
+ this.cloudSyncEnabled = true;
42
+ logger.info('Cloud sync initialized successfully');
43
+ if (this.cloudSyncConfig.syncInterval
44
+ && this.cloudSyncConfig.syncInterval > 0) {
45
+ this.startPeriodicSync();
46
+ }
47
+ }
48
+ catch (error) {
49
+ logger.error('Failed to initialize cloud sync:', error);
50
+ }
51
+ }
52
+ startPeriodicSync() {
53
+ if (!this.cloudSyncConfig?.syncInterval)
54
+ return;
55
+ this.syncIntervalId = window.setInterval(async () => {
56
+ try {
57
+ await this.sync();
58
+ }
59
+ catch (error) {
60
+ logger.error('Periodic sync failed:', error);
61
+ }
62
+ }, this.cloudSyncConfig.syncInterval);
63
+ logger.info(`Started periodic sync every ${this.cloudSyncConfig.syncInterval}ms`);
64
+ }
65
+ /**
66
+ * Stop periodic synchronization
67
+ *
68
+ * @example
69
+ * cloudService.stopPeriodicSync();
70
+ */
71
+ stopPeriodicSync() {
72
+ if (this.syncIntervalId) {
73
+ window.clearInterval(this.syncIntervalId);
74
+ this.syncIntervalId = undefined;
75
+ logger.info('Stopped periodic sync');
76
+ }
77
+ }
78
+ /**
79
+ * Perform manual synchronization
80
+ * @returns Promise that resolves when sync is complete
81
+ * @throws Error if cloud sync is not enabled
82
+ *
83
+ * @example
84
+ * await cloudService.sync();
85
+ */
86
+ async sync() {
87
+ if (!this.cloudSyncEnabled) {
88
+ throw new Error('Cloud sync is not enabled. Make sure cloudSync config is provided.');
89
+ }
90
+ try {
91
+ logger.info('Starting manual sync...');
92
+ await this.db
93
+ .cloud.sync();
94
+ logger.info('Sync completed successfully');
95
+ }
96
+ catch (error) {
97
+ logger.error('Sync failed:', error);
98
+ throw error;
99
+ }
100
+ }
101
+ /**
102
+ * Get current synchronization status
103
+ * @returns Object containing sync status information
104
+ *
105
+ * @example
106
+ * const { enabled, lastSync, isOnline } = cloudService.getSyncStatus();
107
+ */
108
+ getSyncStatus() {
109
+ if (!this.cloudSyncEnabled) {
110
+ return { enabled: false };
111
+ }
112
+ const cloudDb = this.db;
113
+ return {
114
+ enabled: true,
115
+ lastSync: cloudDb.cloud.lastSync
116
+ ? new Date(cloudDb.cloud.lastSync)
117
+ : undefined,
118
+ isOnline: cloudDb.cloud.isOnline,
119
+ };
120
+ }
121
+ /**
122
+ * Enable cloud synchronization
123
+ * @param config - Cloud sync configuration
124
+ * @returns Promise that resolves when cloud sync is enabled
125
+ *
126
+ * @example
127
+ * await cloudService.enableCloudSync({ databaseUrl: 'https://example.cloud' });
128
+ */
129
+ async enableCloudSync(config) {
130
+ if (this.cloudSyncEnabled) {
131
+ logger.warn('Cloud sync is already enabled');
132
+ return;
133
+ }
134
+ await this.initializeCloudSync(config);
135
+ }
136
+ /**
137
+ * Disable cloud synchronization
138
+ *
139
+ * @example
140
+ * cloudService.disableCloudSync();
141
+ */
142
+ disableCloudSync() {
143
+ this.stopPeriodicSync();
144
+ this.cloudSyncEnabled = false;
145
+ this.cloudSyncConfig = undefined;
146
+ logger.info('Cloud sync disabled');
147
+ }
148
+ /**
149
+ * Check if cloud sync is currently enabled
150
+ * @returns True if cloud sync is enabled, false otherwise
151
+ *
152
+ * @example
153
+ * if (cloudService.isCloudSyncEnabled()) {
154
+ * // ...
155
+ * }
156
+ */
157
+ isCloudSyncEnabled() {
158
+ return this.cloudSyncEnabled;
159
+ }
160
+ /**
161
+ * Get current cloud sync configuration
162
+ * @returns Cloud sync configuration or undefined if not set
163
+ *
164
+ * @example
165
+ * const cfg = cloudService.getCloudSyncConfig();
166
+ */
167
+ getCloudSyncConfig() {
168
+ return this.cloudSyncConfig;
169
+ }
170
+ /**
171
+ * Synchronize specific tables
172
+ * @param tableNames - Array of table names to sync
173
+ * @returns Promise that resolves when sync is complete
174
+ * @throws Error if cloud sync is not enabled
175
+ *
176
+ * @example
177
+ * await cloudService.syncTables(['users', 'posts']);
178
+ */
179
+ async syncTables(tableNames) {
180
+ if (!this.cloudSyncEnabled) {
181
+ throw new Error('Cloud sync is not enabled');
182
+ }
183
+ try {
184
+ logger.info(`Syncing tables: ${tableNames.join(', ')}`);
185
+ for (const tableName of tableNames) {
186
+ const table = this.db.table(tableName);
187
+ await table.toCollection().modify(() => { });
188
+ }
189
+ logger.info('Table sync completed');
190
+ }
191
+ catch (error) {
192
+ logger.error('Table sync failed:', error);
193
+ throw error;
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,12 @@
1
+ export type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
2
+ export type LegacyArgs = [Record<'constructor', ClassConstructor>, string];
3
+ export type FieldContext = {
4
+ kind: 'field';
5
+ name: string;
6
+ addInitializer(_init: (_this: {
7
+ constructor: ClassConstructor;
8
+ }) => void): void;
9
+ };
10
+ export type StandardArgs = [unknown, FieldContext];
11
+ export declare function isLegacyArgs(args: unknown[]): args is LegacyArgs;
12
+ export declare function isFieldContext(args: unknown[]): args is StandardArgs;
@@ -0,0 +1,10 @@
1
+ export function isLegacyArgs(args) {
2
+ return Array.isArray(args) && typeof args[1] === 'string';
3
+ }
4
+ export function isFieldContext(args) {
5
+ const ctx = args[1];
6
+ return !!ctx &&
7
+ typeof ctx === 'object' &&
8
+ ctx.kind === 'field' &&
9
+ typeof ctx.name === 'string';
10
+ }
@@ -0,0 +1,25 @@
1
+ import type { BaseEntity } from './BaseEntity';
2
+ /**
3
+ * Create a new entity instance and validate it in a single call.
4
+ *
5
+ * This helper instantiates the provided entity class, assigns the given
6
+ * partial data, and immediately validates it using the entity's
7
+ * `init` method (which calls `validateOrThrow` under the hood).
8
+ *
9
+ * - Preferred construction style matches the project convention:
10
+ * empty constructor followed by property assignment and validation.
11
+ * - If the entity defines a Zod schema, invalid data will cause a
12
+ * ValidationError to be thrown.
13
+ *
14
+ * @typeParam T - Concrete entity type extending BaseEntity
15
+ * @param EntityClass - Entity constructor (must have a zero-arg constructor)
16
+ * @param data - Partial entity data to assign before validation
17
+ * @returns A fully initialized and validated entity instance
18
+ * @throws {import('../errors/ValidationError').ValidationError}
19
+ * If validation fails according to the entity schema
20
+ *
21
+ * @example
22
+ * const user = newEntity(User, { name: 'Alice' });
23
+ * // user is validated; throws if required fields are missing or invalid
24
+ */
25
+ export declare function newEntity<T extends BaseEntity>(EntityClass: new () => T, data: Partial<T>): T;