@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,109 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
|
|
4
|
+
|
|
5
|
+
export type ColumnConfig = {
|
|
6
|
+
required?: boolean;
|
|
7
|
+
unique?: boolean;
|
|
8
|
+
indexed?: boolean;
|
|
9
|
+
default?: unknown | (() => unknown);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RelationConfig = {
|
|
13
|
+
type: 'one-to-one' | 'one-to-many' | 'many-to-many';
|
|
14
|
+
target: ClassConstructor | string;
|
|
15
|
+
foreignKey?: string;
|
|
16
|
+
joinTable?: string;
|
|
17
|
+
cascade?: boolean;
|
|
18
|
+
eager?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DefineEntityOptions = {
|
|
22
|
+
tableName?: string;
|
|
23
|
+
schema?: z.ZodSchema<unknown>;
|
|
24
|
+
timestamps?: boolean;
|
|
25
|
+
columns?: Record<string, ColumnConfig>;
|
|
26
|
+
compoundIndexes?: { name?: string; unique?: boolean; columns: string[] }[];
|
|
27
|
+
relations?: Record<string, RelationConfig>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type StoredEntityMeta = Required<Pick<DefineEntityOptions, 'tableName'>> &
|
|
31
|
+
Omit<DefineEntityOptions, 'tableName'>;
|
|
32
|
+
|
|
33
|
+
const entityMeta = new Map<ClassConstructor, StoredEntityMeta>();
|
|
34
|
+
|
|
35
|
+
const columnMeta = new Map<ClassConstructor, Record<string, ColumnConfig>>();
|
|
36
|
+
const compoundIndexMeta =
|
|
37
|
+
new Map<ClassConstructor, { name: string; unique: boolean; columns: string[] }[]>();
|
|
38
|
+
const relationMeta = new Map<ClassConstructor, Record<string, RelationConfig>>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Define entity metadata without decorators.
|
|
42
|
+
*
|
|
43
|
+
* Registers table name, schema, columns, indexes and relations for a given class
|
|
44
|
+
* so the ORM can work without TypeScript decorators.
|
|
45
|
+
*
|
|
46
|
+
* @param EntityClass - The entity constructor/class
|
|
47
|
+
* @param options - Metadata describing table, schema, columns, indexes and relations
|
|
48
|
+
* @returns void
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* defineEntity(UserEntity, {
|
|
52
|
+
* tableName: 'users',
|
|
53
|
+
* columns: {
|
|
54
|
+
* name: { required: true, indexed: true },
|
|
55
|
+
* email: { required: true, unique: true },
|
|
56
|
+
* },
|
|
57
|
+
* relations: {
|
|
58
|
+
* posts: { type: 'one-to-many', target: PostEntity, foreignKey: 'authorId' }
|
|
59
|
+
* }
|
|
60
|
+
* });
|
|
61
|
+
*/
|
|
62
|
+
export function defineEntity<T>(
|
|
63
|
+
EntityClass: ClassConstructor<T>,
|
|
64
|
+
options: DefineEntityOptions,
|
|
65
|
+
): void {
|
|
66
|
+
const tableName = options.tableName || EntityClass.name.toLowerCase() + 's';
|
|
67
|
+
entityMeta.set(EntityClass, { tableName, ...options });
|
|
68
|
+
|
|
69
|
+
if (options.columns) {
|
|
70
|
+
columnMeta.set(EntityClass, options.columns);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.compoundIndexes) {
|
|
74
|
+
const normalized = options.compoundIndexes.map(ci => ({
|
|
75
|
+
name: ci.name || ci.columns.join('_'),
|
|
76
|
+
unique: !!ci.unique,
|
|
77
|
+
columns: ci.columns,
|
|
78
|
+
}));
|
|
79
|
+
compoundIndexMeta.set(EntityClass, normalized);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.relations) {
|
|
83
|
+
relationMeta.set(EntityClass, options.relations);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getDefinedEntityMeta
|
|
88
|
+
(ctor: ClassConstructor): StoredEntityMeta | undefined {
|
|
89
|
+
return entityMeta.get(ctor);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getDefinedColumns
|
|
93
|
+
(ctor: ClassConstructor): Record<string, ColumnConfig> | undefined {
|
|
94
|
+
return columnMeta.get(ctor);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getDefinedCompoundIndexes(
|
|
98
|
+
ctor: ClassConstructor,
|
|
99
|
+
): { name: string; unique: boolean; columns: string[] }[] | undefined {
|
|
100
|
+
return compoundIndexMeta.get(ctor);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getDefinedRelations(
|
|
104
|
+
ctor: ClassConstructor,
|
|
105
|
+
): Record<string, RelationConfig> | undefined {
|
|
106
|
+
return relationMeta.get(ctor);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { getColumnMetadata } from '../metadata/Column';
|
|
4
|
+
import { getEntityMetadata } from '../metadata/Entity';
|
|
5
|
+
import { BaseEntity } from './BaseEntity';
|
|
6
|
+
import { getDefinedColumns } from './EntityRegistry';
|
|
7
|
+
|
|
8
|
+
export class EntitySchema<T extends BaseEntity = BaseEntity> {
|
|
9
|
+
private entityClass: new () => T;
|
|
10
|
+
private schema?: z.ZodSchema<unknown>;
|
|
11
|
+
|
|
12
|
+
constructor(entityClass: new () => T, schema?: z.ZodSchema<unknown>) {
|
|
13
|
+
this.entityClass = entityClass;
|
|
14
|
+
this.schema = schema;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the table name for this entity
|
|
19
|
+
* @returns The table name (either from metadata or generated from class name)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const schema = new EntitySchema(User);
|
|
23
|
+
* const table = schema.getTableName(); // 'users'
|
|
24
|
+
*/
|
|
25
|
+
getTableName(): string {
|
|
26
|
+
const metadata = getEntityMetadata(this.entityClass);
|
|
27
|
+
|
|
28
|
+
return metadata?.tableName || this.entityClass.name.toLowerCase() + 's';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the Zod schema for this entity
|
|
33
|
+
* @returns The Zod schema or undefined if not defined
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const schema = new EntitySchema(User, userZodSchema);
|
|
37
|
+
* const zod = schema.getSchema();
|
|
38
|
+
*/
|
|
39
|
+
getSchema(): z.ZodSchema<unknown> | undefined {
|
|
40
|
+
return this.schema
|
|
41
|
+
|| (this.entityClass as { schema?: z.ZodSchema<unknown> }).schema;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get column metadata for this entity
|
|
46
|
+
* @returns Object containing column metadata
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const cols = new EntitySchema(User).getColumns();
|
|
50
|
+
* // cols.name.required === true
|
|
51
|
+
*/
|
|
52
|
+
getColumns(): Record<string, unknown> {
|
|
53
|
+
const defined = getDefinedColumns(this.entityClass as unknown as never);
|
|
54
|
+
if (defined) {
|
|
55
|
+
return defined as Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return getColumnMetadata(this.entityClass);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate data against entity schema
|
|
63
|
+
* @param data - Data to validate
|
|
64
|
+
* @returns Validation result with status and errors
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const { isValid, errors } = new EntitySchema(User, userSchema)
|
|
68
|
+
* .validate({ name: 'Alice' });
|
|
69
|
+
*/
|
|
70
|
+
validate(data: Partial<T>): { isValid: boolean; errors: string[] } {
|
|
71
|
+
const schema = this.getSchema();
|
|
72
|
+
|
|
73
|
+
if (!schema) {
|
|
74
|
+
const columns = this.getColumns() as Record<string, { required?: boolean }>;
|
|
75
|
+
const missing: string[] = [];
|
|
76
|
+
|
|
77
|
+
for (const [propertyKey, options] of Object.entries(columns)) {
|
|
78
|
+
if (!options || !options.required) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const value = (data as Record<string, unknown>)[propertyKey];
|
|
83
|
+
|
|
84
|
+
if (value === undefined || value === null) {
|
|
85
|
+
missing.push(propertyKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
return {
|
|
91
|
+
isValid: false,
|
|
92
|
+
errors: missing.map(key => `${key}: is required`),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { isValid: true, errors: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
schema.parse(data);
|
|
101
|
+
|
|
102
|
+
return { isValid: true, errors: [] };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof z.ZodError) {
|
|
105
|
+
const errors = error.issues.map(err => `${err.path.join('.')}: ${err.message}`);
|
|
106
|
+
|
|
107
|
+
return { isValid: false, errors };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { isValid: false, errors: ['Unknown validation error'] };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a new instance of the entity
|
|
116
|
+
* @param data - Optional data to initialize the entity with
|
|
117
|
+
* @returns New entity instance
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const user = new EntitySchema(User).create({ name: 'Bob' });
|
|
121
|
+
*/
|
|
122
|
+
create(data?: Partial<T>): T {
|
|
123
|
+
const instance = new this.entityClass();
|
|
124
|
+
const columns = this.getColumns() as Record<string, { default?: unknown }>;
|
|
125
|
+
|
|
126
|
+
for (const [propertyKey, options] of Object.entries(columns)) {
|
|
127
|
+
const hasValueInData = data
|
|
128
|
+
&& (data as Record<string, unknown>)[propertyKey] !== undefined;
|
|
129
|
+
const hasValueInInstance =
|
|
130
|
+
(instance as Record<string, unknown>)[propertyKey] !== undefined;
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
!hasValueInData &&
|
|
134
|
+
!hasValueInInstance &&
|
|
135
|
+
options &&
|
|
136
|
+
Object.prototype.hasOwnProperty.call(options, 'default')
|
|
137
|
+
) {
|
|
138
|
+
const defaultOption = options.default;
|
|
139
|
+
|
|
140
|
+
const value = typeof defaultOption === 'function'
|
|
141
|
+
? (defaultOption as () => unknown)()
|
|
142
|
+
: defaultOption;
|
|
143
|
+
|
|
144
|
+
(instance as Record<string, unknown>)[propertyKey] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (data) {
|
|
149
|
+
Object.assign(instance, data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return instance;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type Dexie from 'dexie';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EntityConstructor,Migration,
|
|
5
|
+
} from '../types';
|
|
6
|
+
import { createServiceLogger } from '../utils/logger';
|
|
7
|
+
import { SchemaBuilder } from './SchemaBuilder';
|
|
8
|
+
|
|
9
|
+
const logger = createServiceLogger('MigrationManager');
|
|
10
|
+
|
|
11
|
+
export class MigrationManager {
|
|
12
|
+
private dbName: string;
|
|
13
|
+
private currentVersion: number;
|
|
14
|
+
private db: {
|
|
15
|
+
migrationMetadata: {
|
|
16
|
+
get: (_key: string) => Promise<{
|
|
17
|
+
key: string;
|
|
18
|
+
value: string | number;
|
|
19
|
+
updatedAt: number
|
|
20
|
+
} | undefined>;
|
|
21
|
+
put: (_data: {
|
|
22
|
+
key: string;
|
|
23
|
+
value: string | number;
|
|
24
|
+
updatedAt: number
|
|
25
|
+
}) => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
} | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(dbName: string, currentVersion: number) {
|
|
30
|
+
this.dbName = dbName;
|
|
31
|
+
this.currentVersion = currentVersion;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setDatabase(db: {
|
|
35
|
+
migrationMetadata: {
|
|
36
|
+
get: (_key: string) => Promise<{
|
|
37
|
+
key: string;
|
|
38
|
+
value: string | number;
|
|
39
|
+
updatedAt: number
|
|
40
|
+
} | undefined>;
|
|
41
|
+
put: (_data: {
|
|
42
|
+
key: string;
|
|
43
|
+
value: string | number;
|
|
44
|
+
updatedAt: number
|
|
45
|
+
}) => Promise<unknown>;
|
|
46
|
+
};
|
|
47
|
+
}): void {
|
|
48
|
+
this.db = db;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async getLastMigrationVersion(): Promise<number> {
|
|
52
|
+
if (!this.db) {
|
|
53
|
+
logger.error('Database not set in MigrationManager');
|
|
54
|
+
|
|
55
|
+
throw new Error('MigrationManager database not set');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const key = `dexie_orm_migration_${this.dbName}_version`;
|
|
59
|
+
const metadata = await this.db.migrationMetadata.get(key);
|
|
60
|
+
|
|
61
|
+
return metadata?.value as number || 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async setLastMigrationVersion(version: number): Promise<void> {
|
|
65
|
+
if (!this.db) {
|
|
66
|
+
logger.error('Database not set in MigrationManager');
|
|
67
|
+
|
|
68
|
+
throw new Error('MigrationManager database not set');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const key = `dexie_orm_migration_${this.dbName}_version`;
|
|
72
|
+
|
|
73
|
+
await this.db.migrationMetadata.put({
|
|
74
|
+
key,
|
|
75
|
+
value: version,
|
|
76
|
+
updatedAt: Date.now(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async getSchemaHash(): Promise<string | null> {
|
|
81
|
+
if (!this.db) {
|
|
82
|
+
logger.error('Database not set in MigrationManager');
|
|
83
|
+
|
|
84
|
+
throw new Error('MigrationManager database not set');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const key = `dexie_orm_schema_${this.dbName}_hash`;
|
|
88
|
+
const metadata = await this.db.migrationMetadata.get(key);
|
|
89
|
+
|
|
90
|
+
return metadata?.value as string || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async setSchemaHash(hash: string): Promise<void> {
|
|
94
|
+
if (!this.db) {
|
|
95
|
+
logger.error('Database not set in MigrationManager');
|
|
96
|
+
|
|
97
|
+
throw new Error('MigrationManager database not set');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const key = `dexie_orm_schema_${this.dbName}_hash`;
|
|
101
|
+
|
|
102
|
+
await this.db.migrationMetadata.put({
|
|
103
|
+
key,
|
|
104
|
+
value: hash,
|
|
105
|
+
updatedAt: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run database migrations
|
|
111
|
+
* @param migrations - Array of migration objects
|
|
112
|
+
* @param db - Database instance for running migrations
|
|
113
|
+
* @returns Promise that resolves when all migrations are complete
|
|
114
|
+
*/
|
|
115
|
+
async runMigrations(
|
|
116
|
+
migrations: Migration[],
|
|
117
|
+
db: (Dexie & { clearAllData: () => Promise<void> }),
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
logger.info('Running migrations...');
|
|
120
|
+
|
|
121
|
+
const lastMigrationVersion = await this.getLastMigrationVersion();
|
|
122
|
+
|
|
123
|
+
const migrationsToRun = migrations.filter(migration =>
|
|
124
|
+
migration.version > lastMigrationVersion
|
|
125
|
+
&& migration.version <= this.currentVersion,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (migrationsToRun.length === 0) {
|
|
129
|
+
logger.info('No migrations to run');
|
|
130
|
+
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
migrationsToRun.sort((a, b) => a.version - b.version);
|
|
135
|
+
|
|
136
|
+
for (const migration of migrationsToRun) {
|
|
137
|
+
logger.info(`Running migration: ${migration.name} (v${migration.version})`);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await migration.up(db);
|
|
141
|
+
await this.setLastMigrationVersion(migration.version);
|
|
142
|
+
|
|
143
|
+
logger.info(`Migration ${migration.name} completed`);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error(`Migration ${migration.name} failed:`, error);
|
|
146
|
+
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
logger.info('All migrations completed');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
autoRunMigrations(
|
|
155
|
+
migrations: Migration[],
|
|
156
|
+
db: (Dexie & { clearAllData: () => Promise<void> }),
|
|
157
|
+
): void {
|
|
158
|
+
setTimeout(async () => {
|
|
159
|
+
try {
|
|
160
|
+
await this.runMigrations(migrations, db);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error('Error during auto-migration:', error);
|
|
163
|
+
}
|
|
164
|
+
}, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Perform selective reset for changed tables only
|
|
169
|
+
* @param entities - Array of entity constructors
|
|
170
|
+
* @param db - Database instance for clearing tables
|
|
171
|
+
* @returns Promise that resolves when selective reset is complete
|
|
172
|
+
*/
|
|
173
|
+
async performSelectiveReset(
|
|
174
|
+
entities: EntityConstructor[],
|
|
175
|
+
db: Dexie,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
logger.info('Performing selective reset for changed tables...');
|
|
178
|
+
|
|
179
|
+
const currentTableSchemas = SchemaBuilder.buildTableSchemas(entities);
|
|
180
|
+
const storedTableSchemas = SchemaBuilder.getStoredTableSchemas(this.dbName);
|
|
181
|
+
|
|
182
|
+
const changes = SchemaBuilder
|
|
183
|
+
.compareSchemas(storedTableSchemas, currentTableSchemas);
|
|
184
|
+
|
|
185
|
+
if (changes.length === 0) {
|
|
186
|
+
logger.info('No schema changes detected for selective reset');
|
|
187
|
+
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const change of changes) {
|
|
192
|
+
if (change.changeType === 'removed' || change.changeType === 'modified') {
|
|
193
|
+
logger.info(`Clearing ${change.changeType} table: ${change.tableName}`);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await db.table(change.tableName).clear();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.error(`Could not clear table ${change.tableName}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
SchemaBuilder.storeTableSchemas(this.dbName, currentTableSchemas);
|
|
204
|
+
|
|
205
|
+
logger.info('Selective reset complete');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
autoSelectiveReset(
|
|
209
|
+
entities: EntityConstructor[],
|
|
210
|
+
db: Dexie,
|
|
211
|
+
): void {
|
|
212
|
+
setTimeout(async () => {
|
|
213
|
+
try {
|
|
214
|
+
await this.performSelectiveReset(entities, db);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error('Error during auto-selective reset:', error);
|
|
217
|
+
}
|
|
218
|
+
}, 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reset entire database by clearing all data
|
|
223
|
+
* @param db - Database instance for clearing data
|
|
224
|
+
* @returns Promise that resolves when database is reset
|
|
225
|
+
*/
|
|
226
|
+
async resetDatabase(db: { clearAllData: () => Promise<void> }): Promise<void> {
|
|
227
|
+
logger.info('Resetting database due to schema changes...');
|
|
228
|
+
|
|
229
|
+
await db.clearAllData();
|
|
230
|
+
|
|
231
|
+
await this.setLastMigrationVersion(0);
|
|
232
|
+
await this.setSchemaHash('');
|
|
233
|
+
|
|
234
|
+
logger.info('Database reset complete');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Automatically reset database asynchronously
|
|
239
|
+
* @param db - Database instance for clearing data
|
|
240
|
+
*/
|
|
241
|
+
autoResetDatabase(db: { clearAllData: () => Promise<void> }): void {
|
|
242
|
+
setTimeout(async () => {
|
|
243
|
+
try {
|
|
244
|
+
await this.resetDatabase(db);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.error('Error during auto-reset:', error);
|
|
247
|
+
}
|
|
248
|
+
}, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if schema has changed and reset database if needed
|
|
253
|
+
* @param entities - Array of entity constructors
|
|
254
|
+
* @param db - Database instance for clearing data
|
|
255
|
+
* @returns Promise resolving to true if schema changed, false otherwise
|
|
256
|
+
*/
|
|
257
|
+
async checkSchemaChanges(
|
|
258
|
+
entities: EntityConstructor[],
|
|
259
|
+
db: { clearAllData: () => Promise<void> },
|
|
260
|
+
): Promise<boolean> {
|
|
261
|
+
const currentSchema = SchemaBuilder.buildSchema(entities);
|
|
262
|
+
const schemaHash = SchemaBuilder.generateSchemaHash(currentSchema);
|
|
263
|
+
const storedHash = await this.getSchemaHash();
|
|
264
|
+
|
|
265
|
+
if (storedHash && storedHash !== schemaHash) {
|
|
266
|
+
logger.info('Schema changes detected!');
|
|
267
|
+
|
|
268
|
+
await this.resetDatabase(db);
|
|
269
|
+
await this.setSchemaHash(schemaHash);
|
|
270
|
+
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|