@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a new entity instance and validate it in a single call.
|
|
3
|
+
*
|
|
4
|
+
* This helper instantiates the provided entity class, assigns the given
|
|
5
|
+
* partial data, and immediately validates it using the entity's
|
|
6
|
+
* `init` method (which calls `validateOrThrow` under the hood).
|
|
7
|
+
*
|
|
8
|
+
* - Preferred construction style matches the project convention:
|
|
9
|
+
* empty constructor followed by property assignment and validation.
|
|
10
|
+
* - If the entity defines a Zod schema, invalid data will cause a
|
|
11
|
+
* ValidationError to be thrown.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam T - Concrete entity type extending BaseEntity
|
|
14
|
+
* @param EntityClass - Entity constructor (must have a zero-arg constructor)
|
|
15
|
+
* @param data - Partial entity data to assign before validation
|
|
16
|
+
* @returns A fully initialized and validated entity instance
|
|
17
|
+
* @throws {import('../errors/ValidationError').ValidationError}
|
|
18
|
+
* If validation fails according to the entity schema
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const user = newEntity(User, { name: 'Alice' });
|
|
22
|
+
* // user is validated; throws if required fields are missing or invalid
|
|
23
|
+
*/
|
|
24
|
+
export function newEntity(EntityClass, data) {
|
|
25
|
+
const instance = new EntityClass();
|
|
26
|
+
return instance.init(data);
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
|
|
3
|
+
export type ColumnConfig = {
|
|
4
|
+
required?: boolean;
|
|
5
|
+
unique?: boolean;
|
|
6
|
+
indexed?: boolean;
|
|
7
|
+
default?: unknown | (() => unknown);
|
|
8
|
+
};
|
|
9
|
+
export type RelationConfig = {
|
|
10
|
+
type: 'one-to-one' | 'one-to-many' | 'many-to-many';
|
|
11
|
+
target: ClassConstructor | string;
|
|
12
|
+
foreignKey?: string;
|
|
13
|
+
joinTable?: string;
|
|
14
|
+
cascade?: boolean;
|
|
15
|
+
eager?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type DefineEntityOptions = {
|
|
18
|
+
tableName?: string;
|
|
19
|
+
schema?: z.ZodSchema<unknown>;
|
|
20
|
+
timestamps?: boolean;
|
|
21
|
+
columns?: Record<string, ColumnConfig>;
|
|
22
|
+
compoundIndexes?: {
|
|
23
|
+
name?: string;
|
|
24
|
+
unique?: boolean;
|
|
25
|
+
columns: string[];
|
|
26
|
+
}[];
|
|
27
|
+
relations?: Record<string, RelationConfig>;
|
|
28
|
+
};
|
|
29
|
+
type StoredEntityMeta = Required<Pick<DefineEntityOptions, 'tableName'>> & Omit<DefineEntityOptions, 'tableName'>;
|
|
30
|
+
/**
|
|
31
|
+
* Define entity metadata without decorators.
|
|
32
|
+
*
|
|
33
|
+
* Registers table name, schema, columns, indexes and relations for a given class
|
|
34
|
+
* so the ORM can work without TypeScript decorators.
|
|
35
|
+
*
|
|
36
|
+
* @param EntityClass - The entity constructor/class
|
|
37
|
+
* @param options - Metadata describing table, schema, columns, indexes and relations
|
|
38
|
+
* @returns void
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* defineEntity(UserEntity, {
|
|
42
|
+
* tableName: 'users',
|
|
43
|
+
* columns: {
|
|
44
|
+
* name: { required: true, indexed: true },
|
|
45
|
+
* email: { required: true, unique: true },
|
|
46
|
+
* },
|
|
47
|
+
* relations: {
|
|
48
|
+
* posts: { type: 'one-to-many', target: PostEntity, foreignKey: 'authorId' }
|
|
49
|
+
* }
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
export declare function defineEntity<T>(EntityClass: ClassConstructor<T>, options: DefineEntityOptions): void;
|
|
53
|
+
export declare function getDefinedEntityMeta(ctor: ClassConstructor): StoredEntityMeta | undefined;
|
|
54
|
+
export declare function getDefinedColumns(ctor: ClassConstructor): Record<string, ColumnConfig> | undefined;
|
|
55
|
+
export declare function getDefinedCompoundIndexes(ctor: ClassConstructor): {
|
|
56
|
+
name: string;
|
|
57
|
+
unique: boolean;
|
|
58
|
+
columns: string[];
|
|
59
|
+
}[] | undefined;
|
|
60
|
+
export declare function getDefinedRelations(ctor: ClassConstructor): Record<string, RelationConfig> | undefined;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const entityMeta = new Map();
|
|
2
|
+
const columnMeta = new Map();
|
|
3
|
+
const compoundIndexMeta = new Map();
|
|
4
|
+
const relationMeta = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Define entity metadata without decorators.
|
|
7
|
+
*
|
|
8
|
+
* Registers table name, schema, columns, indexes and relations for a given class
|
|
9
|
+
* so the ORM can work without TypeScript decorators.
|
|
10
|
+
*
|
|
11
|
+
* @param EntityClass - The entity constructor/class
|
|
12
|
+
* @param options - Metadata describing table, schema, columns, indexes and relations
|
|
13
|
+
* @returns void
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* defineEntity(UserEntity, {
|
|
17
|
+
* tableName: 'users',
|
|
18
|
+
* columns: {
|
|
19
|
+
* name: { required: true, indexed: true },
|
|
20
|
+
* email: { required: true, unique: true },
|
|
21
|
+
* },
|
|
22
|
+
* relations: {
|
|
23
|
+
* posts: { type: 'one-to-many', target: PostEntity, foreignKey: 'authorId' }
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export function defineEntity(EntityClass, options) {
|
|
28
|
+
const tableName = options.tableName || EntityClass.name.toLowerCase() + 's';
|
|
29
|
+
entityMeta.set(EntityClass, { tableName, ...options });
|
|
30
|
+
if (options.columns) {
|
|
31
|
+
columnMeta.set(EntityClass, options.columns);
|
|
32
|
+
}
|
|
33
|
+
if (options.compoundIndexes) {
|
|
34
|
+
const normalized = options.compoundIndexes.map(ci => ({
|
|
35
|
+
name: ci.name || ci.columns.join('_'),
|
|
36
|
+
unique: !!ci.unique,
|
|
37
|
+
columns: ci.columns,
|
|
38
|
+
}));
|
|
39
|
+
compoundIndexMeta.set(EntityClass, normalized);
|
|
40
|
+
}
|
|
41
|
+
if (options.relations) {
|
|
42
|
+
relationMeta.set(EntityClass, options.relations);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getDefinedEntityMeta(ctor) {
|
|
46
|
+
return entityMeta.get(ctor);
|
|
47
|
+
}
|
|
48
|
+
export function getDefinedColumns(ctor) {
|
|
49
|
+
return columnMeta.get(ctor);
|
|
50
|
+
}
|
|
51
|
+
export function getDefinedCompoundIndexes(ctor) {
|
|
52
|
+
return compoundIndexMeta.get(ctor);
|
|
53
|
+
}
|
|
54
|
+
export function getDefinedRelations(ctor) {
|
|
55
|
+
return relationMeta.get(ctor);
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BaseEntity } from './BaseEntity';
|
|
3
|
+
export declare class EntitySchema<T extends BaseEntity = BaseEntity> {
|
|
4
|
+
private entityClass;
|
|
5
|
+
private schema?;
|
|
6
|
+
constructor(entityClass: new () => T, schema?: z.ZodSchema<unknown>);
|
|
7
|
+
/**
|
|
8
|
+
* Get the table name for this entity
|
|
9
|
+
* @returns The table name (either from metadata or generated from class name)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const schema = new EntitySchema(User);
|
|
13
|
+
* const table = schema.getTableName(); // 'users'
|
|
14
|
+
*/
|
|
15
|
+
getTableName(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get the Zod schema for this entity
|
|
18
|
+
* @returns The Zod schema or undefined if not defined
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const schema = new EntitySchema(User, userZodSchema);
|
|
22
|
+
* const zod = schema.getSchema();
|
|
23
|
+
*/
|
|
24
|
+
getSchema(): z.ZodSchema<unknown> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Get column metadata for this entity
|
|
27
|
+
* @returns Object containing column metadata
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const cols = new EntitySchema(User).getColumns();
|
|
31
|
+
* // cols.name.required === true
|
|
32
|
+
*/
|
|
33
|
+
getColumns(): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Validate data against entity schema
|
|
36
|
+
* @param data - Data to validate
|
|
37
|
+
* @returns Validation result with status and errors
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const { isValid, errors } = new EntitySchema(User, userSchema)
|
|
41
|
+
* .validate({ name: 'Alice' });
|
|
42
|
+
*/
|
|
43
|
+
validate(data: Partial<T>): {
|
|
44
|
+
isValid: boolean;
|
|
45
|
+
errors: string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Create a new instance of the entity
|
|
49
|
+
* @param data - Optional data to initialize the entity with
|
|
50
|
+
* @returns New entity instance
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const user = new EntitySchema(User).create({ name: 'Bob' });
|
|
54
|
+
*/
|
|
55
|
+
create(data?: Partial<T>): T;
|
|
56
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getColumnMetadata } from '../metadata/Column';
|
|
3
|
+
import { getEntityMetadata } from '../metadata/Entity';
|
|
4
|
+
import { getDefinedColumns } from './EntityRegistry';
|
|
5
|
+
export class EntitySchema {
|
|
6
|
+
entityClass;
|
|
7
|
+
schema;
|
|
8
|
+
constructor(entityClass, schema) {
|
|
9
|
+
this.entityClass = entityClass;
|
|
10
|
+
this.schema = schema;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the table name for this entity
|
|
14
|
+
* @returns The table name (either from metadata or generated from class name)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const schema = new EntitySchema(User);
|
|
18
|
+
* const table = schema.getTableName(); // 'users'
|
|
19
|
+
*/
|
|
20
|
+
getTableName() {
|
|
21
|
+
const metadata = getEntityMetadata(this.entityClass);
|
|
22
|
+
return metadata?.tableName || this.entityClass.name.toLowerCase() + 's';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the Zod schema for this entity
|
|
26
|
+
* @returns The Zod schema or undefined if not defined
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const schema = new EntitySchema(User, userZodSchema);
|
|
30
|
+
* const zod = schema.getSchema();
|
|
31
|
+
*/
|
|
32
|
+
getSchema() {
|
|
33
|
+
return this.schema
|
|
34
|
+
|| this.entityClass.schema;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get column metadata for this entity
|
|
38
|
+
* @returns Object containing column metadata
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const cols = new EntitySchema(User).getColumns();
|
|
42
|
+
* // cols.name.required === true
|
|
43
|
+
*/
|
|
44
|
+
getColumns() {
|
|
45
|
+
const defined = getDefinedColumns(this.entityClass);
|
|
46
|
+
if (defined) {
|
|
47
|
+
return defined;
|
|
48
|
+
}
|
|
49
|
+
return getColumnMetadata(this.entityClass);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate data against entity schema
|
|
53
|
+
* @param data - Data to validate
|
|
54
|
+
* @returns Validation result with status and errors
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const { isValid, errors } = new EntitySchema(User, userSchema)
|
|
58
|
+
* .validate({ name: 'Alice' });
|
|
59
|
+
*/
|
|
60
|
+
validate(data) {
|
|
61
|
+
const schema = this.getSchema();
|
|
62
|
+
if (!schema) {
|
|
63
|
+
const columns = this.getColumns();
|
|
64
|
+
const missing = [];
|
|
65
|
+
for (const [propertyKey, options] of Object.entries(columns)) {
|
|
66
|
+
if (!options || !options.required) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const value = data[propertyKey];
|
|
70
|
+
if (value === undefined || value === null) {
|
|
71
|
+
missing.push(propertyKey);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (missing.length > 0) {
|
|
75
|
+
return {
|
|
76
|
+
isValid: false,
|
|
77
|
+
errors: missing.map(key => `${key}: is required`),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { isValid: true, errors: [] };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
schema.parse(data);
|
|
84
|
+
return { isValid: true, errors: [] };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof z.ZodError) {
|
|
88
|
+
const errors = error.issues.map(err => `${err.path.join('.')}: ${err.message}`);
|
|
89
|
+
return { isValid: false, errors };
|
|
90
|
+
}
|
|
91
|
+
return { isValid: false, errors: ['Unknown validation error'] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a new instance of the entity
|
|
96
|
+
* @param data - Optional data to initialize the entity with
|
|
97
|
+
* @returns New entity instance
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* const user = new EntitySchema(User).create({ name: 'Bob' });
|
|
101
|
+
*/
|
|
102
|
+
create(data) {
|
|
103
|
+
const instance = new this.entityClass();
|
|
104
|
+
const columns = this.getColumns();
|
|
105
|
+
for (const [propertyKey, options] of Object.entries(columns)) {
|
|
106
|
+
const hasValueInData = data
|
|
107
|
+
&& data[propertyKey] !== undefined;
|
|
108
|
+
const hasValueInInstance = instance[propertyKey] !== undefined;
|
|
109
|
+
if (!hasValueInData &&
|
|
110
|
+
!hasValueInInstance &&
|
|
111
|
+
options &&
|
|
112
|
+
Object.prototype.hasOwnProperty.call(options, 'default')) {
|
|
113
|
+
const defaultOption = options.default;
|
|
114
|
+
const value = typeof defaultOption === 'function'
|
|
115
|
+
? defaultOption()
|
|
116
|
+
: defaultOption;
|
|
117
|
+
instance[propertyKey] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (data) {
|
|
121
|
+
Object.assign(instance, data);
|
|
122
|
+
}
|
|
123
|
+
return instance;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type Dexie from 'dexie';
|
|
2
|
+
import type { EntityConstructor, Migration } from '../types';
|
|
3
|
+
export declare class MigrationManager {
|
|
4
|
+
private dbName;
|
|
5
|
+
private currentVersion;
|
|
6
|
+
private db;
|
|
7
|
+
constructor(dbName: string, currentVersion: number);
|
|
8
|
+
setDatabase(db: {
|
|
9
|
+
migrationMetadata: {
|
|
10
|
+
get: (_key: string) => Promise<{
|
|
11
|
+
key: string;
|
|
12
|
+
value: string | number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
} | undefined>;
|
|
15
|
+
put: (_data: {
|
|
16
|
+
key: string;
|
|
17
|
+
value: string | number;
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
}) => Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
}): void;
|
|
22
|
+
private getLastMigrationVersion;
|
|
23
|
+
private setLastMigrationVersion;
|
|
24
|
+
private getSchemaHash;
|
|
25
|
+
private setSchemaHash;
|
|
26
|
+
/**
|
|
27
|
+
* Run database migrations
|
|
28
|
+
* @param migrations - Array of migration objects
|
|
29
|
+
* @param db - Database instance for running migrations
|
|
30
|
+
* @returns Promise that resolves when all migrations are complete
|
|
31
|
+
*/
|
|
32
|
+
runMigrations(migrations: Migration[], db: (Dexie & {
|
|
33
|
+
clearAllData: () => Promise<void>;
|
|
34
|
+
})): Promise<void>;
|
|
35
|
+
autoRunMigrations(migrations: Migration[], db: (Dexie & {
|
|
36
|
+
clearAllData: () => Promise<void>;
|
|
37
|
+
})): void;
|
|
38
|
+
/**
|
|
39
|
+
* Perform selective reset for changed tables only
|
|
40
|
+
* @param entities - Array of entity constructors
|
|
41
|
+
* @param db - Database instance for clearing tables
|
|
42
|
+
* @returns Promise that resolves when selective reset is complete
|
|
43
|
+
*/
|
|
44
|
+
performSelectiveReset(entities: EntityConstructor[], db: Dexie): Promise<void>;
|
|
45
|
+
autoSelectiveReset(entities: EntityConstructor[], db: Dexie): void;
|
|
46
|
+
/**
|
|
47
|
+
* Reset entire database by clearing all data
|
|
48
|
+
* @param db - Database instance for clearing data
|
|
49
|
+
* @returns Promise that resolves when database is reset
|
|
50
|
+
*/
|
|
51
|
+
resetDatabase(db: {
|
|
52
|
+
clearAllData: () => Promise<void>;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Automatically reset database asynchronously
|
|
56
|
+
* @param db - Database instance for clearing data
|
|
57
|
+
*/
|
|
58
|
+
autoResetDatabase(db: {
|
|
59
|
+
clearAllData: () => Promise<void>;
|
|
60
|
+
}): void;
|
|
61
|
+
/**
|
|
62
|
+
* Check if schema has changed and reset database if needed
|
|
63
|
+
* @param entities - Array of entity constructors
|
|
64
|
+
* @param db - Database instance for clearing data
|
|
65
|
+
* @returns Promise resolving to true if schema changed, false otherwise
|
|
66
|
+
*/
|
|
67
|
+
checkSchemaChanges(entities: EntityConstructor[], db: {
|
|
68
|
+
clearAllData: () => Promise<void>;
|
|
69
|
+
}): Promise<boolean>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createServiceLogger } from '../utils/logger';
|
|
2
|
+
import { SchemaBuilder } from './SchemaBuilder';
|
|
3
|
+
const logger = createServiceLogger('MigrationManager');
|
|
4
|
+
export class MigrationManager {
|
|
5
|
+
dbName;
|
|
6
|
+
currentVersion;
|
|
7
|
+
db = null;
|
|
8
|
+
constructor(dbName, currentVersion) {
|
|
9
|
+
this.dbName = dbName;
|
|
10
|
+
this.currentVersion = currentVersion;
|
|
11
|
+
}
|
|
12
|
+
setDatabase(db) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
}
|
|
15
|
+
async getLastMigrationVersion() {
|
|
16
|
+
if (!this.db) {
|
|
17
|
+
logger.error('Database not set in MigrationManager');
|
|
18
|
+
throw new Error('MigrationManager database not set');
|
|
19
|
+
}
|
|
20
|
+
const key = `dexie_orm_migration_${this.dbName}_version`;
|
|
21
|
+
const metadata = await this.db.migrationMetadata.get(key);
|
|
22
|
+
return metadata?.value || 0;
|
|
23
|
+
}
|
|
24
|
+
async setLastMigrationVersion(version) {
|
|
25
|
+
if (!this.db) {
|
|
26
|
+
logger.error('Database not set in MigrationManager');
|
|
27
|
+
throw new Error('MigrationManager database not set');
|
|
28
|
+
}
|
|
29
|
+
const key = `dexie_orm_migration_${this.dbName}_version`;
|
|
30
|
+
await this.db.migrationMetadata.put({
|
|
31
|
+
key,
|
|
32
|
+
value: version,
|
|
33
|
+
updatedAt: Date.now(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async getSchemaHash() {
|
|
37
|
+
if (!this.db) {
|
|
38
|
+
logger.error('Database not set in MigrationManager');
|
|
39
|
+
throw new Error('MigrationManager database not set');
|
|
40
|
+
}
|
|
41
|
+
const key = `dexie_orm_schema_${this.dbName}_hash`;
|
|
42
|
+
const metadata = await this.db.migrationMetadata.get(key);
|
|
43
|
+
return metadata?.value || null;
|
|
44
|
+
}
|
|
45
|
+
async setSchemaHash(hash) {
|
|
46
|
+
if (!this.db) {
|
|
47
|
+
logger.error('Database not set in MigrationManager');
|
|
48
|
+
throw new Error('MigrationManager database not set');
|
|
49
|
+
}
|
|
50
|
+
const key = `dexie_orm_schema_${this.dbName}_hash`;
|
|
51
|
+
await this.db.migrationMetadata.put({
|
|
52
|
+
key,
|
|
53
|
+
value: hash,
|
|
54
|
+
updatedAt: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run database migrations
|
|
59
|
+
* @param migrations - Array of migration objects
|
|
60
|
+
* @param db - Database instance for running migrations
|
|
61
|
+
* @returns Promise that resolves when all migrations are complete
|
|
62
|
+
*/
|
|
63
|
+
async runMigrations(migrations, db) {
|
|
64
|
+
logger.info('Running migrations...');
|
|
65
|
+
const lastMigrationVersion = await this.getLastMigrationVersion();
|
|
66
|
+
const migrationsToRun = migrations.filter(migration => migration.version > lastMigrationVersion
|
|
67
|
+
&& migration.version <= this.currentVersion);
|
|
68
|
+
if (migrationsToRun.length === 0) {
|
|
69
|
+
logger.info('No migrations to run');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
migrationsToRun.sort((a, b) => a.version - b.version);
|
|
73
|
+
for (const migration of migrationsToRun) {
|
|
74
|
+
logger.info(`Running migration: ${migration.name} (v${migration.version})`);
|
|
75
|
+
try {
|
|
76
|
+
await migration.up(db);
|
|
77
|
+
await this.setLastMigrationVersion(migration.version);
|
|
78
|
+
logger.info(`Migration ${migration.name} completed`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error(`Migration ${migration.name} failed:`, error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
logger.info('All migrations completed');
|
|
86
|
+
}
|
|
87
|
+
autoRunMigrations(migrations, db) {
|
|
88
|
+
setTimeout(async () => {
|
|
89
|
+
try {
|
|
90
|
+
await this.runMigrations(migrations, db);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
logger.error('Error during auto-migration:', error);
|
|
94
|
+
}
|
|
95
|
+
}, 0);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Perform selective reset for changed tables only
|
|
99
|
+
* @param entities - Array of entity constructors
|
|
100
|
+
* @param db - Database instance for clearing tables
|
|
101
|
+
* @returns Promise that resolves when selective reset is complete
|
|
102
|
+
*/
|
|
103
|
+
async performSelectiveReset(entities, db) {
|
|
104
|
+
logger.info('Performing selective reset for changed tables...');
|
|
105
|
+
const currentTableSchemas = SchemaBuilder.buildTableSchemas(entities);
|
|
106
|
+
const storedTableSchemas = SchemaBuilder.getStoredTableSchemas(this.dbName);
|
|
107
|
+
const changes = SchemaBuilder
|
|
108
|
+
.compareSchemas(storedTableSchemas, currentTableSchemas);
|
|
109
|
+
if (changes.length === 0) {
|
|
110
|
+
logger.info('No schema changes detected for selective reset');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const change of changes) {
|
|
114
|
+
if (change.changeType === 'removed' || change.changeType === 'modified') {
|
|
115
|
+
logger.info(`Clearing ${change.changeType} table: ${change.tableName}`);
|
|
116
|
+
try {
|
|
117
|
+
await db.table(change.tableName).clear();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger.error(`Could not clear table ${change.tableName}:`, error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
SchemaBuilder.storeTableSchemas(this.dbName, currentTableSchemas);
|
|
125
|
+
logger.info('Selective reset complete');
|
|
126
|
+
}
|
|
127
|
+
autoSelectiveReset(entities, db) {
|
|
128
|
+
setTimeout(async () => {
|
|
129
|
+
try {
|
|
130
|
+
await this.performSelectiveReset(entities, db);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
logger.error('Error during auto-selective reset:', error);
|
|
134
|
+
}
|
|
135
|
+
}, 0);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Reset entire database by clearing all data
|
|
139
|
+
* @param db - Database instance for clearing data
|
|
140
|
+
* @returns Promise that resolves when database is reset
|
|
141
|
+
*/
|
|
142
|
+
async resetDatabase(db) {
|
|
143
|
+
logger.info('Resetting database due to schema changes...');
|
|
144
|
+
await db.clearAllData();
|
|
145
|
+
await this.setLastMigrationVersion(0);
|
|
146
|
+
await this.setSchemaHash('');
|
|
147
|
+
logger.info('Database reset complete');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Automatically reset database asynchronously
|
|
151
|
+
* @param db - Database instance for clearing data
|
|
152
|
+
*/
|
|
153
|
+
autoResetDatabase(db) {
|
|
154
|
+
setTimeout(async () => {
|
|
155
|
+
try {
|
|
156
|
+
await this.resetDatabase(db);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger.error('Error during auto-reset:', error);
|
|
160
|
+
}
|
|
161
|
+
}, 0);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if schema has changed and reset database if needed
|
|
165
|
+
* @param entities - Array of entity constructors
|
|
166
|
+
* @param db - Database instance for clearing data
|
|
167
|
+
* @returns Promise resolving to true if schema changed, false otherwise
|
|
168
|
+
*/
|
|
169
|
+
async checkSchemaChanges(entities, db) {
|
|
170
|
+
const currentSchema = SchemaBuilder.buildSchema(entities);
|
|
171
|
+
const schemaHash = SchemaBuilder.generateSchemaHash(currentSchema);
|
|
172
|
+
const storedHash = await this.getSchemaHash();
|
|
173
|
+
if (storedHash && storedHash !== schemaHash) {
|
|
174
|
+
logger.info('Schema changes detected!');
|
|
175
|
+
await this.resetDatabase(db);
|
|
176
|
+
await this.setSchemaHash(schemaHash);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EntityConstructor } from '../types';
|
|
2
|
+
import { BaseEntity } from './BaseEntity';
|
|
3
|
+
export declare class RelationLoader {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(database: {
|
|
6
|
+
table: (_name: string) => unknown;
|
|
7
|
+
});
|
|
8
|
+
private resolveTableName;
|
|
9
|
+
/**
|
|
10
|
+
* Load relations for an entity
|
|
11
|
+
* @param entity - The entity to load relations for
|
|
12
|
+
* @param entityClass - The entity class
|
|
13
|
+
* @param relationNames - Optional array of specific relation names to load.
|
|
14
|
+
* If not provided, loads all eager relations.
|
|
15
|
+
*/
|
|
16
|
+
loadRelations<T extends BaseEntity>(entity: T, entityClass: EntityConstructor<T>, relationNames?: string[]): Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Load a specific relation by name
|
|
19
|
+
* @param entity - The entity to load relation for
|
|
20
|
+
* @param entityClass - The entity class
|
|
21
|
+
* @param relationName - The name of the relation to load
|
|
22
|
+
* @returns Promise resolving to the loaded relation data
|
|
23
|
+
*/
|
|
24
|
+
loadRelationByName<T extends BaseEntity, K extends keyof T>(entity: T, entityClass: EntityConstructor<T>, relationName: K): Promise<T[K]>;
|
|
25
|
+
/**
|
|
26
|
+
* Load a specific relation based on metadata
|
|
27
|
+
* @param entity - The entity to load relation for
|
|
28
|
+
* @param relationMeta - The relation metadata containing:
|
|
29
|
+
* type, target, and configuration
|
|
30
|
+
* @returns Promise resolving to the loaded relation data
|
|
31
|
+
*/
|
|
32
|
+
loadRelation<T extends BaseEntity>(entity: T, relationMeta: {
|
|
33
|
+
type: string;
|
|
34
|
+
target: string | EntityConstructor;
|
|
35
|
+
foreignKey?: string;
|
|
36
|
+
joinTable?: string;
|
|
37
|
+
}): Promise<unknown>;
|
|
38
|
+
private loadOneToOne;
|
|
39
|
+
private loadOneToMany;
|
|
40
|
+
/**
|
|
41
|
+
* Load many-to-many relation
|
|
42
|
+
*/
|
|
43
|
+
private loadManyToMany;
|
|
44
|
+
/**
|
|
45
|
+
* Save entity with all its relations
|
|
46
|
+
* @param entity - The entity to save with relations
|
|
47
|
+
* @param entityClass - The entity class
|
|
48
|
+
* @returns Promise resolving to the saved entity with relations
|
|
49
|
+
*/
|
|
50
|
+
saveWithRelations<T extends BaseEntity>(entity: T, entityClass: EntityConstructor<T>): Promise<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Delete entity with cascade handling for relations
|
|
53
|
+
* @param entity - The entity to delete
|
|
54
|
+
* @param entityClass - The entity class
|
|
55
|
+
*/
|
|
56
|
+
deleteWithRelations<T extends BaseEntity>(entity: T, entityClass: EntityConstructor<T>): Promise<void>;
|
|
57
|
+
private deleteRelation;
|
|
58
|
+
private deleteOneToOne;
|
|
59
|
+
private deleteOneToMany;
|
|
60
|
+
private deleteManyToMany;
|
|
61
|
+
private saveEntity;
|
|
62
|
+
private saveRelation;
|
|
63
|
+
private saveOneToOne;
|
|
64
|
+
private saveOneToMany;
|
|
65
|
+
private saveManyToMany;
|
|
66
|
+
}
|