@indexeddb-orm/idb-orm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.vscode/extensions.json +5 -0
  2. package/README.md +1280 -0
  3. package/angular-demo-app/README.md +84 -0
  4. package/angular-demo-app/angular.json +109 -0
  5. package/angular-demo-app/package-lock.json +14215 -0
  6. package/angular-demo-app/package.json +41 -0
  7. package/angular-demo-app/src/app/app.component.ts +481 -0
  8. package/angular-demo-app/src/app/app.routes.ts +8 -0
  9. package/angular-demo-app/src/app/components/actions.component.ts +202 -0
  10. package/angular-demo-app/src/app/components/cloud-sync-demo.component.ts +296 -0
  11. package/angular-demo-app/src/app/components/live-query-demo.component.ts +307 -0
  12. package/angular-demo-app/src/app/components/main-info.component.ts +148 -0
  13. package/angular-demo-app/src/app/components/posts-live-query-demo.component.ts +336 -0
  14. package/angular-demo-app/src/app/components/typescript-demo.component.ts +268 -0
  15. package/angular-demo-app/src/entities/post-tag.entity.ts +25 -0
  16. package/angular-demo-app/src/entities/post.entity.ts +49 -0
  17. package/angular-demo-app/src/entities/profile.entity.ts +42 -0
  18. package/angular-demo-app/src/entities/tag.entity.ts +36 -0
  19. package/angular-demo-app/src/entities/user.entity.ts +59 -0
  20. package/angular-demo-app/src/favicon.ico +1 -0
  21. package/angular-demo-app/src/index.html +16 -0
  22. package/angular-demo-app/src/main.ts +13 -0
  23. package/angular-demo-app/src/services/app-logic.service.ts +449 -0
  24. package/angular-demo-app/src/services/cloud-sync.service.ts +95 -0
  25. package/angular-demo-app/src/services/database.service.ts +26 -0
  26. package/angular-demo-app/src/services/live-query.service.ts +63 -0
  27. package/angular-demo-app/src/services/posts-live-query.service.ts +86 -0
  28. package/angular-demo-app/src/services/typescript-demo.service.ts +59 -0
  29. package/angular-demo-app/src/styles.scss +50 -0
  30. package/angular-demo-app/tsconfig.app.json +13 -0
  31. package/angular-demo-app/tsconfig.json +34 -0
  32. package/angular-demo-app/tsconfig.spec.json +13 -0
  33. package/dist/Database.d.ts +206 -0
  34. package/dist/Database.js +288 -0
  35. package/dist/decorators/Column.d.ts +79 -0
  36. package/dist/decorators/Column.js +236 -0
  37. package/dist/decorators/Entity.d.ts +32 -0
  38. package/dist/decorators/Entity.js +44 -0
  39. package/dist/decorators/Relation.d.ts +70 -0
  40. package/dist/decorators/Relation.js +120 -0
  41. package/dist/decorators/index.d.ts +3 -0
  42. package/dist/decorators/index.js +3 -0
  43. package/dist/errors/ValidationError.d.ts +4 -0
  44. package/dist/errors/ValidationError.js +8 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +7 -0
  47. package/dist/metadata/Column.d.ts +8 -0
  48. package/dist/metadata/Column.js +44 -0
  49. package/dist/metadata/Entity.d.ts +11 -0
  50. package/dist/metadata/Entity.js +21 -0
  51. package/dist/metadata/Relation.d.ts +20 -0
  52. package/dist/metadata/Relation.js +74 -0
  53. package/dist/metadata/index.d.ts +3 -0
  54. package/dist/metadata/index.js +3 -0
  55. package/dist/services/AggregationService.d.ts +38 -0
  56. package/dist/services/AggregationService.js +229 -0
  57. package/dist/services/BaseEntity.d.ts +32 -0
  58. package/dist/services/BaseEntity.js +62 -0
  59. package/dist/services/CloudSyncService.d.ts +100 -0
  60. package/dist/services/CloudSyncService.js +196 -0
  61. package/dist/services/DecoratorUtils.d.ts +12 -0
  62. package/dist/services/DecoratorUtils.js +10 -0
  63. package/dist/services/EntityFactory.d.ts +25 -0
  64. package/dist/services/EntityFactory.js +27 -0
  65. package/dist/services/EntityRegistry.d.ts +61 -0
  66. package/dist/services/EntityRegistry.js +56 -0
  67. package/dist/services/EntitySchema.d.ts +56 -0
  68. package/dist/services/EntitySchema.js +125 -0
  69. package/dist/services/MigrationManager.d.ts +70 -0
  70. package/dist/services/MigrationManager.js +181 -0
  71. package/dist/services/RelationLoader.d.ts +66 -0
  72. package/dist/services/RelationLoader.js +310 -0
  73. package/dist/services/SchemaBuilder.d.ts +68 -0
  74. package/dist/services/SchemaBuilder.js +191 -0
  75. package/dist/services/index.d.ts +7 -0
  76. package/dist/services/index.js +7 -0
  77. package/dist/types.d.ts +152 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/logger.d.ts +12 -0
  80. package/dist/utils/logger.js +16 -0
  81. package/eslint.config.js +49 -0
  82. package/homepage/favicon.svg +36 -0
  83. package/homepage/index.html +1725 -0
  84. package/package.json +78 -0
  85. package/react-demo-app/README.md +61 -0
  86. package/react-demo-app/eslint.config.js +60 -0
  87. package/react-demo-app/index.html +13 -0
  88. package/react-demo-app/package-lock.json +4955 -0
  89. package/react-demo-app/package.json +39 -0
  90. package/react-demo-app/src/App.tsx +172 -0
  91. package/react-demo-app/src/assets/react.svg +1 -0
  92. package/react-demo-app/src/components/Actions.tsx +171 -0
  93. package/react-demo-app/src/components/CloudSyncDemo.tsx +191 -0
  94. package/react-demo-app/src/components/LiveQueryDemo.tsx +122 -0
  95. package/react-demo-app/src/components/MainInfo.tsx +75 -0
  96. package/react-demo-app/src/components/PostsLiveQueryDemo.tsx +185 -0
  97. package/react-demo-app/src/components/TypeScriptDemo.tsx +190 -0
  98. package/react-demo-app/src/database/Database.ts +30 -0
  99. package/react-demo-app/src/entities/Post.ts +48 -0
  100. package/react-demo-app/src/entities/PostTag.ts +26 -0
  101. package/react-demo-app/src/entities/Profile.ts +41 -0
  102. package/react-demo-app/src/entities/Tag.ts +35 -0
  103. package/react-demo-app/src/entities/User.ts +61 -0
  104. package/react-demo-app/src/hooks/useAppLogic.ts +565 -0
  105. package/react-demo-app/src/hooks/useCloudSyncDemo.ts +84 -0
  106. package/react-demo-app/src/hooks/useLiveQueryDemo.ts +68 -0
  107. package/react-demo-app/src/hooks/usePostsLiveQueryDemo.ts +64 -0
  108. package/react-demo-app/src/hooks/useTypeScriptDemo.ts +43 -0
  109. package/react-demo-app/src/index.css +26 -0
  110. package/react-demo-app/src/main.tsx +18 -0
  111. package/react-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
  112. package/react-demo-app/src/migrations/002-add-post-category.ts +37 -0
  113. package/react-demo-app/src/migrations/index.ts +8 -0
  114. package/react-demo-app/src/vite-env.d.ts +1 -0
  115. package/react-demo-app/tsconfig.app.json +22 -0
  116. package/react-demo-app/tsconfig.json +6 -0
  117. package/react-demo-app/vite.config.ts +10 -0
  118. package/src/Database.ts +405 -0
  119. package/src/errors/ValidationError.ts +9 -0
  120. package/src/index.ts +13 -0
  121. package/src/metadata/Column.ts +74 -0
  122. package/src/metadata/Entity.ts +42 -0
  123. package/src/metadata/Relation.ts +121 -0
  124. package/src/metadata/index.ts +5 -0
  125. package/src/services/AggregationService.ts +348 -0
  126. package/src/services/BaseEntity.ts +77 -0
  127. package/src/services/CloudSyncService.ts +248 -0
  128. package/src/services/EntityFactory.ts +35 -0
  129. package/src/services/EntityRegistry.ts +109 -0
  130. package/src/services/EntitySchema.ts +154 -0
  131. package/src/services/MigrationManager.ts +276 -0
  132. package/src/services/RelationLoader.ts +532 -0
  133. package/src/services/SchemaBuilder.ts +237 -0
  134. package/src/services/index.ts +7 -0
  135. package/src/types.d.ts +1 -0
  136. package/src/types.ts +169 -0
  137. package/src/utils/logger.ts +40 -0
  138. package/svelte-demo-app/README.md +61 -0
  139. package/svelte-demo-app/package-lock.json +3000 -0
  140. package/svelte-demo-app/package.json +30 -0
  141. package/svelte-demo-app/src/app.d.ts +12 -0
  142. package/svelte-demo-app/src/app.html +13 -0
  143. package/svelte-demo-app/src/components/Actions.svelte +121 -0
  144. package/svelte-demo-app/src/components/CloudSyncDemo.svelte +333 -0
  145. package/svelte-demo-app/src/components/LiveQueryDemo.svelte +191 -0
  146. package/svelte-demo-app/src/components/MainInfo.svelte +133 -0
  147. package/svelte-demo-app/src/components/PostsLiveQueryDemo.svelte +330 -0
  148. package/svelte-demo-app/src/components/TypeScriptDemo.svelte +251 -0
  149. package/svelte-demo-app/src/database/Database.ts +29 -0
  150. package/svelte-demo-app/src/entities/Post.ts +46 -0
  151. package/svelte-demo-app/src/entities/PostTag.ts +22 -0
  152. package/svelte-demo-app/src/entities/Profile.ts +39 -0
  153. package/svelte-demo-app/src/entities/Tag.ts +33 -0
  154. package/svelte-demo-app/src/entities/User.ts +62 -0
  155. package/svelte-demo-app/src/lib/database/Database.ts +30 -0
  156. package/svelte-demo-app/src/lib/entities/Post.ts +47 -0
  157. package/svelte-demo-app/src/lib/entities/PostTag.ts +23 -0
  158. package/svelte-demo-app/src/lib/entities/Profile.ts +40 -0
  159. package/svelte-demo-app/src/lib/entities/Tag.ts +34 -0
  160. package/svelte-demo-app/src/lib/entities/User.ts +59 -0
  161. package/svelte-demo-app/src/lib/index.ts +7 -0
  162. package/svelte-demo-app/src/lib/migrations/001-add-user-email-index.ts +17 -0
  163. package/svelte-demo-app/src/lib/migrations/002-add-post-category.ts +37 -0
  164. package/svelte-demo-app/src/lib/migrations/index.ts +8 -0
  165. package/svelte-demo-app/src/migrations/001-add-user-email-index.ts +17 -0
  166. package/svelte-demo-app/src/migrations/002-add-post-category.ts +37 -0
  167. package/svelte-demo-app/src/migrations/index.ts +8 -0
  168. package/svelte-demo-app/src/routes/+layout.js +3 -0
  169. package/svelte-demo-app/src/routes/+layout.svelte +228 -0
  170. package/svelte-demo-app/src/routes/+page.js +3 -0
  171. package/svelte-demo-app/src/routes/+page.svelte +1305 -0
  172. package/svelte-demo-app/src/stores/appStore.js +603 -0
  173. package/svelte-demo-app/svelte.config.js +18 -0
  174. package/svelte-demo-app/tsconfig.json +14 -0
  175. package/svelte-demo-app/vite.config.ts +6 -0
  176. package/tests/aggregation.e2e.test.ts +87 -0
  177. package/tests/base-entity.e2e.test.ts +47 -0
  178. package/tests/database-api.e2e.test.ts +177 -0
  179. package/tests/decorators.e2e.test.ts +40 -0
  180. package/tests/entity-schema.e2e.test.ts +58 -0
  181. package/tests/relation-loader-table-names.test.ts +192 -0
  182. package/tests/relations.e2e.test.ts +178 -0
  183. package/tests/zod-runtime.e2e.test.ts +69 -0
  184. package/tsconfig.json +21 -0
  185. package/vitest.config.ts +21 -0
  186. package/vitest.setup.ts +27 -0
  187. package/vue-demo-app/README.md +61 -0
  188. package/vue-demo-app/index.html +13 -0
  189. package/vue-demo-app/package-lock.json +1537 -0
  190. package/vue-demo-app/package.json +27 -0
  191. package/vue-demo-app/src/App.vue +100 -0
  192. package/vue-demo-app/src/components/Actions.vue +135 -0
  193. package/vue-demo-app/src/components/CloudSyncDemo.vue +139 -0
  194. package/vue-demo-app/src/components/LiveQueryDemo.vue +122 -0
  195. package/vue-demo-app/src/components/MainInfo.vue +80 -0
  196. package/vue-demo-app/src/components/PostsLiveQueryDemo.vue +136 -0
  197. package/vue-demo-app/src/components/TypeScriptDemo.vue +133 -0
  198. package/vue-demo-app/src/database/Database.ts +29 -0
  199. package/vue-demo-app/src/entities/Post.ts +48 -0
  200. package/vue-demo-app/src/entities/PostTag.ts +24 -0
  201. package/vue-demo-app/src/entities/Profile.ts +41 -0
  202. package/vue-demo-app/src/entities/Tag.ts +35 -0
  203. package/vue-demo-app/src/entities/User.ts +61 -0
  204. package/vue-demo-app/src/main.ts +29 -0
  205. package/vue-demo-app/src/migrations/001-add-user-email-index.ts +23 -0
  206. package/vue-demo-app/src/migrations/002-add-post-category.ts +46 -0
  207. package/vue-demo-app/src/migrations/index.ts +14 -0
  208. package/vue-demo-app/src/services/useAppLogic.ts +565 -0
  209. package/vue-demo-app/src/services/useCloudSyncDemo.ts +84 -0
  210. package/vue-demo-app/src/services/useLiveQueryDemo.ts +82 -0
  211. package/vue-demo-app/src/services/usePostsLiveQueryDemo.ts +77 -0
  212. package/vue-demo-app/src/services/useTypeScriptDemo.ts +56 -0
  213. package/vue-demo-app/src/vite-env.d.ts +1 -0
  214. package/vue-demo-app/tsconfig.json +25 -0
  215. package/vue-demo-app/tsconfig.node.json +10 -0
  216. package/vue-demo-app/vite.config.ts +16 -0
@@ -0,0 +1,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
+ }