@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,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';
|