@indexeddb-orm/idb-orm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/extensions.json +5 -0
- package/README.md +1280 -0
- package/angular-demo-app/README.md +84 -0
- package/angular-demo-app/angular.json +109 -0
- package/angular-demo-app/package-lock.json +14215 -0
- package/angular-demo-app/package.json +41 -0
- package/angular-demo-app/src/app/app.component.ts +481 -0
- package/angular-demo-app/src/app/app.routes.ts +8 -0
- package/angular-demo-app/src/app/components/actions.component.ts +202 -0
- package/angular-demo-app/src/app/components/cloud-sync-demo.component.ts +296 -0
- package/angular-demo-app/src/app/components/live-query-demo.component.ts +307 -0
- package/angular-demo-app/src/app/components/main-info.component.ts +148 -0
- package/angular-demo-app/src/app/components/posts-live-query-demo.component.ts +336 -0
- package/angular-demo-app/src/app/components/typescript-demo.component.ts +268 -0
- package/angular-demo-app/src/entities/post-tag.entity.ts +25 -0
- package/angular-demo-app/src/entities/post.entity.ts +49 -0
- package/angular-demo-app/src/entities/profile.entity.ts +42 -0
- package/angular-demo-app/src/entities/tag.entity.ts +36 -0
- package/angular-demo-app/src/entities/user.entity.ts +59 -0
- package/angular-demo-app/src/favicon.ico +1 -0
- package/angular-demo-app/src/index.html +16 -0
- package/angular-demo-app/src/main.ts +13 -0
- package/angular-demo-app/src/services/app-logic.service.ts +449 -0
- package/angular-demo-app/src/services/cloud-sync.service.ts +95 -0
- package/angular-demo-app/src/services/database.service.ts +26 -0
- package/angular-demo-app/src/services/live-query.service.ts +63 -0
- package/angular-demo-app/src/services/posts-live-query.service.ts +86 -0
- package/angular-demo-app/src/services/typescript-demo.service.ts +59 -0
- package/angular-demo-app/src/styles.scss +50 -0
- package/angular-demo-app/tsconfig.app.json +13 -0
- package/angular-demo-app/tsconfig.json +34 -0
- package/angular-demo-app/tsconfig.spec.json +13 -0
- package/dist/Database.d.ts +206 -0
- package/dist/Database.js +288 -0
- package/dist/decorators/Column.d.ts +79 -0
- package/dist/decorators/Column.js +236 -0
- package/dist/decorators/Entity.d.ts +32 -0
- package/dist/decorators/Entity.js +44 -0
- package/dist/decorators/Relation.d.ts +70 -0
- package/dist/decorators/Relation.js +120 -0
- package/dist/decorators/index.d.ts +3 -0
- package/dist/decorators/index.js +3 -0
- package/dist/errors/ValidationError.d.ts +4 -0
- package/dist/errors/ValidationError.js +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/metadata/Column.d.ts +8 -0
- package/dist/metadata/Column.js +44 -0
- package/dist/metadata/Entity.d.ts +11 -0
- package/dist/metadata/Entity.js +21 -0
- package/dist/metadata/Relation.d.ts +20 -0
- package/dist/metadata/Relation.js +74 -0
- package/dist/metadata/index.d.ts +3 -0
- package/dist/metadata/index.js +3 -0
- package/dist/services/AggregationService.d.ts +38 -0
- package/dist/services/AggregationService.js +229 -0
- package/dist/services/BaseEntity.d.ts +32 -0
- package/dist/services/BaseEntity.js +62 -0
- package/dist/services/CloudSyncService.d.ts +100 -0
- package/dist/services/CloudSyncService.js +196 -0
- package/dist/services/DecoratorUtils.d.ts +12 -0
- package/dist/services/DecoratorUtils.js +10 -0
- package/dist/services/EntityFactory.d.ts +25 -0
- package/dist/services/EntityFactory.js +27 -0
- package/dist/services/EntityRegistry.d.ts +61 -0
- package/dist/services/EntityRegistry.js +56 -0
- package/dist/services/EntitySchema.d.ts +56 -0
- package/dist/services/EntitySchema.js +125 -0
- package/dist/services/MigrationManager.d.ts +70 -0
- package/dist/services/MigrationManager.js +181 -0
- package/dist/services/RelationLoader.d.ts +66 -0
- package/dist/services/RelationLoader.js +310 -0
- package/dist/services/SchemaBuilder.d.ts +68 -0
- package/dist/services/SchemaBuilder.js +191 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.js +7 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +16 -0
- package/eslint.config.js +49 -0
- package/homepage/favicon.svg +36 -0
- package/homepage/index.html +1725 -0
- package/package.json +78 -0
- package/react-demo-app/README.md +61 -0
- package/react-demo-app/eslint.config.js +60 -0
- package/react-demo-app/index.html +13 -0
- package/react-demo-app/package-lock.json +4955 -0
- package/react-demo-app/package.json +39 -0
- package/react-demo-app/src/App.tsx +172 -0
- package/react-demo-app/src/assets/react.svg +1 -0
- package/react-demo-app/src/components/Actions.tsx +171 -0
- package/react-demo-app/src/components/CloudSyncDemo.tsx +191 -0
- package/react-demo-app/src/components/LiveQueryDemo.tsx +122 -0
- package/react-demo-app/src/components/MainInfo.tsx +75 -0
- package/react-demo-app/src/components/PostsLiveQueryDemo.tsx +185 -0
- package/react-demo-app/src/components/TypeScriptDemo.tsx +190 -0
- package/react-demo-app/src/database/Database.ts +30 -0
- package/react-demo-app/src/entities/Post.ts +48 -0
- package/react-demo-app/src/entities/PostTag.ts +26 -0
- package/react-demo-app/src/entities/Profile.ts +41 -0
- package/react-demo-app/src/entities/Tag.ts +35 -0
- package/react-demo-app/src/entities/User.ts +61 -0
- package/react-demo-app/src/hooks/useAppLogic.ts +565 -0
- package/react-demo-app/src/hooks/useCloudSyncDemo.ts +84 -0
- package/react-demo-app/src/hooks/useLiveQueryDemo.ts +68 -0
- package/react-demo-app/src/hooks/usePostsLiveQueryDemo.ts +64 -0
- package/react-demo-app/src/hooks/useTypeScriptDemo.ts +43 -0
- package/react-demo-app/src/index.css +26 -0
- package/react-demo-app/src/main.tsx +18 -0
- package/react-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
- package/react-demo-app/src/migrations/002-add-post-category.ts +37 -0
- package/react-demo-app/src/migrations/index.ts +8 -0
- package/react-demo-app/src/vite-env.d.ts +1 -0
- package/react-demo-app/tsconfig.app.json +22 -0
- package/react-demo-app/tsconfig.json +6 -0
- package/react-demo-app/vite.config.ts +10 -0
- package/src/Database.ts +405 -0
- package/src/errors/ValidationError.ts +9 -0
- package/src/index.ts +13 -0
- package/src/metadata/Column.ts +74 -0
- package/src/metadata/Entity.ts +42 -0
- package/src/metadata/Relation.ts +121 -0
- package/src/metadata/index.ts +5 -0
- package/src/services/AggregationService.ts +348 -0
- package/src/services/BaseEntity.ts +77 -0
- package/src/services/CloudSyncService.ts +248 -0
- package/src/services/EntityFactory.ts +35 -0
- package/src/services/EntityRegistry.ts +109 -0
- package/src/services/EntitySchema.ts +154 -0
- package/src/services/MigrationManager.ts +276 -0
- package/src/services/RelationLoader.ts +532 -0
- package/src/services/SchemaBuilder.ts +237 -0
- package/src/services/index.ts +7 -0
- package/src/types.d.ts +1 -0
- package/src/types.ts +169 -0
- package/src/utils/logger.ts +40 -0
- package/svelte-demo-app/README.md +61 -0
- package/svelte-demo-app/package-lock.json +3000 -0
- package/svelte-demo-app/package.json +30 -0
- package/svelte-demo-app/src/app.d.ts +12 -0
- package/svelte-demo-app/src/app.html +13 -0
- package/svelte-demo-app/src/components/Actions.svelte +121 -0
- package/svelte-demo-app/src/components/CloudSyncDemo.svelte +333 -0
- package/svelte-demo-app/src/components/LiveQueryDemo.svelte +191 -0
- package/svelte-demo-app/src/components/MainInfo.svelte +133 -0
- package/svelte-demo-app/src/components/PostsLiveQueryDemo.svelte +330 -0
- package/svelte-demo-app/src/components/TypeScriptDemo.svelte +251 -0
- package/svelte-demo-app/src/database/Database.ts +29 -0
- package/svelte-demo-app/src/entities/Post.ts +46 -0
- package/svelte-demo-app/src/entities/PostTag.ts +22 -0
- package/svelte-demo-app/src/entities/Profile.ts +39 -0
- package/svelte-demo-app/src/entities/Tag.ts +33 -0
- package/svelte-demo-app/src/entities/User.ts +62 -0
- package/svelte-demo-app/src/lib/database/Database.ts +30 -0
- package/svelte-demo-app/src/lib/entities/Post.ts +47 -0
- package/svelte-demo-app/src/lib/entities/PostTag.ts +23 -0
- package/svelte-demo-app/src/lib/entities/Profile.ts +40 -0
- package/svelte-demo-app/src/lib/entities/Tag.ts +34 -0
- package/svelte-demo-app/src/lib/entities/User.ts +59 -0
- package/svelte-demo-app/src/lib/index.ts +7 -0
- package/svelte-demo-app/src/lib/migrations/001-add-user-email-index.ts +17 -0
- package/svelte-demo-app/src/lib/migrations/002-add-post-category.ts +37 -0
- package/svelte-demo-app/src/lib/migrations/index.ts +8 -0
- package/svelte-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
- package/svelte-demo-app/src/migrations/002-add-post-category.ts +37 -0
- package/svelte-demo-app/src/migrations/index.ts +8 -0
- package/svelte-demo-app/src/routes/+layout.js +3 -0
- package/svelte-demo-app/src/routes/+layout.svelte +228 -0
- package/svelte-demo-app/src/routes/+page.js +3 -0
- package/svelte-demo-app/src/routes/+page.svelte +1305 -0
- package/svelte-demo-app/src/stores/appStore.js +603 -0
- package/svelte-demo-app/svelte.config.js +18 -0
- package/svelte-demo-app/tsconfig.json +14 -0
- package/svelte-demo-app/vite.config.ts +6 -0
- package/tests/aggregation.e2e.test.ts +87 -0
- package/tests/base-entity.e2e.test.ts +47 -0
- package/tests/database-api.e2e.test.ts +177 -0
- package/tests/decorators.e2e.test.ts +40 -0
- package/tests/entity-schema.e2e.test.ts +58 -0
- package/tests/relation-loader-table-names.test.ts +192 -0
- package/tests/relations.e2e.test.ts +178 -0
- package/tests/zod-runtime.e2e.test.ts +69 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +21 -0
- package/vitest.setup.ts +27 -0
- package/vue-demo-app/README.md +61 -0
- package/vue-demo-app/index.html +13 -0
- package/vue-demo-app/package-lock.json +1537 -0
- package/vue-demo-app/package.json +27 -0
- package/vue-demo-app/src/App.vue +100 -0
- package/vue-demo-app/src/components/Actions.vue +135 -0
- package/vue-demo-app/src/components/CloudSyncDemo.vue +139 -0
- package/vue-demo-app/src/components/LiveQueryDemo.vue +122 -0
- package/vue-demo-app/src/components/MainInfo.vue +80 -0
- package/vue-demo-app/src/components/PostsLiveQueryDemo.vue +136 -0
- package/vue-demo-app/src/components/TypeScriptDemo.vue +133 -0
- package/vue-demo-app/src/database/Database.ts +29 -0
- package/vue-demo-app/src/entities/Post.ts +48 -0
- package/vue-demo-app/src/entities/PostTag.ts +24 -0
- package/vue-demo-app/src/entities/Profile.ts +41 -0
- package/vue-demo-app/src/entities/Tag.ts +35 -0
- package/vue-demo-app/src/entities/User.ts +61 -0
- package/vue-demo-app/src/main.ts +29 -0
- package/vue-demo-app/src/migrations/001-add-user-email-index.ts +23 -0
- package/vue-demo-app/src/migrations/002-add-post-category.ts +46 -0
- package/vue-demo-app/src/migrations/index.ts +14 -0
- package/vue-demo-app/src/services/useAppLogic.ts +565 -0
- package/vue-demo-app/src/services/useCloudSyncDemo.ts +84 -0
- package/vue-demo-app/src/services/useLiveQueryDemo.ts +82 -0
- package/vue-demo-app/src/services/usePostsLiveQueryDemo.ts +77 -0
- package/vue-demo-app/src/services/useTypeScriptDemo.ts +56 -0
- package/vue-demo-app/src/vite-env.d.ts +1 -0
- package/vue-demo-app/tsconfig.json +25 -0
- package/vue-demo-app/tsconfig.node.json +10 -0
- package/vue-demo-app/vite.config.ts +16 -0
|
@@ -0,0 +1,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;
|