@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
package/src/Database.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import Dexie, { type Table } from 'dexie';
|
|
2
|
+
|
|
3
|
+
import { getEntityMetadata } from './metadata/Entity';
|
|
4
|
+
import { AggregationService } from './services/AggregationService';
|
|
5
|
+
import { BaseEntity } from './services/BaseEntity';
|
|
6
|
+
import { CloudSyncService } from './services/CloudSyncService';
|
|
7
|
+
import { MigrationManager } from './services/MigrationManager';
|
|
8
|
+
import { RelationLoader } from './services/RelationLoader';
|
|
9
|
+
import { SchemaBuilder } from './services/SchemaBuilder';
|
|
10
|
+
import type {
|
|
11
|
+
AggregationOptions,
|
|
12
|
+
AggregationResult,
|
|
13
|
+
CloudSyncConfig,
|
|
14
|
+
DatabaseConfig,
|
|
15
|
+
EntityConstructor,
|
|
16
|
+
Migration,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { logger } from './utils/logger';
|
|
19
|
+
|
|
20
|
+
export class Database extends Dexie {
|
|
21
|
+
private entitySchemas = new Map<string, EntityConstructor>();
|
|
22
|
+
private migrationManager: MigrationManager;
|
|
23
|
+
private relationLoader: RelationLoader;
|
|
24
|
+
private aggregationService: AggregationService;
|
|
25
|
+
private cloudSyncService: CloudSyncService;
|
|
26
|
+
|
|
27
|
+
migrationMetadata!: Table<{
|
|
28
|
+
key: string;
|
|
29
|
+
value: string | number;
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
constructor(config: DatabaseConfig) {
|
|
34
|
+
super(config.name);
|
|
35
|
+
|
|
36
|
+
this.migrationManager = new MigrationManager(config.name, config.version);
|
|
37
|
+
this.relationLoader = new RelationLoader(this);
|
|
38
|
+
this.aggregationService = new AggregationService(this);
|
|
39
|
+
this.cloudSyncService = new CloudSyncService(this);
|
|
40
|
+
|
|
41
|
+
const currentSchema = SchemaBuilder.buildSchema(config.entities);
|
|
42
|
+
|
|
43
|
+
const schemaHash = SchemaBuilder.generateSchemaHash(currentSchema);
|
|
44
|
+
const storedHash = localStorage.getItem(`${config.name}_schema_hash`);
|
|
45
|
+
|
|
46
|
+
let version = config.version;
|
|
47
|
+
|
|
48
|
+
if (storedHash && storedHash !== schemaHash) {
|
|
49
|
+
logger.info('Schema changed, incrementing version...');
|
|
50
|
+
|
|
51
|
+
version = config.version + 1;
|
|
52
|
+
localStorage.setItem(`${config.name}_schema_hash`, schemaHash);
|
|
53
|
+
|
|
54
|
+
if (config.migrations && config.migrations.length > 0) {
|
|
55
|
+
logger.info('Running migrations due to schema change...');
|
|
56
|
+
|
|
57
|
+
this.migrationManager.autoRunMigrations(config.migrations, this);
|
|
58
|
+
} else {
|
|
59
|
+
if (config.onSchemaChangeStrategy === 'all') {
|
|
60
|
+
logger.info('Auto-resetting entire database due to schema change...');
|
|
61
|
+
|
|
62
|
+
this.migrationManager.autoResetDatabase(this);
|
|
63
|
+
} else if (config.onSchemaChangeStrategy === 'selective') {
|
|
64
|
+
logger.info('Auto-resetting only changed tables...');
|
|
65
|
+
|
|
66
|
+
this.migrationManager.autoSelectiveReset(config.entities, this);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.version(version).stores(currentSchema);
|
|
72
|
+
this.migrationManager.setDatabase(this);
|
|
73
|
+
|
|
74
|
+
config.entities.forEach(entity => {
|
|
75
|
+
const metadata = getEntityMetadata(entity);
|
|
76
|
+
const tableName = metadata?.tableName || entity.name.toLowerCase() + 's';
|
|
77
|
+
this.entitySchemas.set(tableName, entity);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (config.cloudSync) {
|
|
81
|
+
this.cloudSyncService.initializeCloudSync(config.cloudSync);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get repository for entity (TypeORM style)
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const users = db.getRepository(User);
|
|
90
|
+
* const id = await users.add({ name: 'Ann' } as User);
|
|
91
|
+
*/
|
|
92
|
+
getRepository<
|
|
93
|
+
T extends BaseEntity,
|
|
94
|
+
TKey extends Extract<
|
|
95
|
+
NonNullable<T['id']>,
|
|
96
|
+
string | number
|
|
97
|
+
> = Extract<NonNullable<T['id']>, string | number>
|
|
98
|
+
>(entityClass: EntityConstructor<T>): Table<T, TKey> {
|
|
99
|
+
const fromRegistry = Array.from(this.entitySchemas.entries()).find(
|
|
100
|
+
([, ctor]) => ctor === entityClass,
|
|
101
|
+
)?.[0];
|
|
102
|
+
|
|
103
|
+
const metadata = getEntityMetadata(entityClass);
|
|
104
|
+
let tableName = fromRegistry
|
|
105
|
+
|| metadata?.tableName
|
|
106
|
+
|| entityClass.name.toLowerCase() + 's';
|
|
107
|
+
|
|
108
|
+
if (!fromRegistry && !metadata?.tableName) {
|
|
109
|
+
const n = entityClass.name;
|
|
110
|
+
if (/Entity$/i.test(n)) {
|
|
111
|
+
const base = n.replace(/Entity$/i, '');
|
|
112
|
+
tableName = base.toLowerCase() + 's';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.table(tableName) as Table<T, TKey>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Perform aggregation on entity data
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const result = await db.aggregate({
|
|
124
|
+
* entityClass: Post,
|
|
125
|
+
* options: { where: { category: 'tech' } },
|
|
126
|
+
* });
|
|
127
|
+
*/
|
|
128
|
+
async aggregate<T extends BaseEntity>(params: {
|
|
129
|
+
entityClass: EntityConstructor<T>;
|
|
130
|
+
options: AggregationOptions<T>;
|
|
131
|
+
}): Promise<AggregationResult> {
|
|
132
|
+
return this.aggregationService.aggregate(params.entityClass, params.options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create database with entity registration
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const db = Database.createDatabase({
|
|
140
|
+
* name: 'app-db',
|
|
141
|
+
* version: 1,
|
|
142
|
+
* entities: [User, Post],
|
|
143
|
+
* });
|
|
144
|
+
*/
|
|
145
|
+
static createDatabase(params: {
|
|
146
|
+
name: string;
|
|
147
|
+
version: number;
|
|
148
|
+
entities: EntityConstructor[];
|
|
149
|
+
config?: Partial<DatabaseConfig>;
|
|
150
|
+
}): Database {
|
|
151
|
+
const db = new Database({
|
|
152
|
+
name: params.name,
|
|
153
|
+
version: params.version,
|
|
154
|
+
entities: params.entities,
|
|
155
|
+
onSchemaChangeStrategy: params.config?.onSchemaChangeStrategy,
|
|
156
|
+
migrations: params.config?.migrations,
|
|
157
|
+
cloudSync: params.config?.cloudSync,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return db as Database;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Clear all data from database
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* await db.clearAllData();
|
|
168
|
+
*/
|
|
169
|
+
async clearAllData(): Promise<void> {
|
|
170
|
+
const tableNames = this.tables.map(table => table.name);
|
|
171
|
+
|
|
172
|
+
await Promise.all(tableNames.map(name => this.table(name).clear()));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reset database when schema changes
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* await db.resetDatabase();
|
|
180
|
+
*/
|
|
181
|
+
async resetDatabase(): Promise<void> {
|
|
182
|
+
await this.migrationManager.resetDatabase(this);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if schema has changed and reset if needed
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* const changed = await db.checkSchemaChanges();
|
|
190
|
+
*/
|
|
191
|
+
async checkSchemaChanges(): Promise<boolean> {
|
|
192
|
+
return this.migrationManager.checkSchemaChanges(
|
|
193
|
+
Array.from(this.entitySchemas.values()),
|
|
194
|
+
this,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Manually perform selective reset for changed tables only
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* await db.performSelectiveReset();
|
|
203
|
+
*/
|
|
204
|
+
async performSelectiveReset(): Promise<void> {
|
|
205
|
+
await this.migrationManager.performSelectiveReset(
|
|
206
|
+
Array.from(this.entitySchemas.values()),
|
|
207
|
+
this,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run migrations for schema changes
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* await db.runMigrations(migrations);
|
|
216
|
+
*/
|
|
217
|
+
async runMigrations(migrations: Migration[]): Promise<void> {
|
|
218
|
+
await this.migrationManager.runMigrations(migrations, this);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get typed table for entity
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* const posts = db.getTypedTable(Post);
|
|
226
|
+
*/
|
|
227
|
+
getTypedTable<
|
|
228
|
+
T extends BaseEntity,
|
|
229
|
+
TKey extends Extract<
|
|
230
|
+
NonNullable<T['id']>,
|
|
231
|
+
string | number
|
|
232
|
+
> = Extract<NonNullable<T['id']>, string | number>
|
|
233
|
+
>(entityClass: EntityConstructor<T>): Table<T, TKey> {
|
|
234
|
+
const metadata = getEntityMetadata(entityClass);
|
|
235
|
+
const tableName =
|
|
236
|
+
metadata?.tableName || entityClass.name.toLowerCase() + 's';
|
|
237
|
+
|
|
238
|
+
return this.table(tableName) as Table<T, TKey>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get table with proper typing for specific entity
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* const users = db.getTableForEntity(User);
|
|
246
|
+
*/
|
|
247
|
+
getTableForEntity<
|
|
248
|
+
T extends BaseEntity,
|
|
249
|
+
TKey extends Extract<
|
|
250
|
+
NonNullable<T['id']>,
|
|
251
|
+
string | number
|
|
252
|
+
> = Extract<NonNullable<T['id']>, string | number>
|
|
253
|
+
>(entityClass: EntityConstructor<T>): Table<T, TKey> {
|
|
254
|
+
return this.getTypedTable<T, TKey>(entityClass);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get all entities
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const all = db.getEntities();
|
|
262
|
+
*/
|
|
263
|
+
getEntities(): EntityConstructor[] {
|
|
264
|
+
return Array.from(this.entitySchemas.values());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get entity by table name
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const UserClass = db.getEntity('users');
|
|
272
|
+
*/
|
|
273
|
+
getEntity(tableName: string): EntityConstructor | undefined {
|
|
274
|
+
return this.entitySchemas.get(tableName);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Load relations for an entity
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* const userWithRelations = await db.loadRelations({
|
|
282
|
+
* entity: user,
|
|
283
|
+
* entityClass: User,
|
|
284
|
+
* relationNames: ['posts'],
|
|
285
|
+
* });
|
|
286
|
+
*/
|
|
287
|
+
async loadRelations<T extends BaseEntity>(params: {
|
|
288
|
+
entity: T;
|
|
289
|
+
entityClass: EntityConstructor<T>;
|
|
290
|
+
relationNames?: string[];
|
|
291
|
+
}): Promise<T> {
|
|
292
|
+
return this.relationLoader.loadRelations(
|
|
293
|
+
params.entity,
|
|
294
|
+
params.entityClass,
|
|
295
|
+
params.relationNames,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Load a specific relation by name
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* const posts = await db.loadRelationByName({
|
|
304
|
+
* entity: { id: userId },
|
|
305
|
+
* entityClass: User,
|
|
306
|
+
* relationName: 'posts',
|
|
307
|
+
* });
|
|
308
|
+
*/
|
|
309
|
+
async loadRelationByName<
|
|
310
|
+
T extends BaseEntity,
|
|
311
|
+
K extends keyof T
|
|
312
|
+
>(params: {
|
|
313
|
+
entity: T | { id: string | number };
|
|
314
|
+
entityClass: EntityConstructor<T>;
|
|
315
|
+
relationName: K;
|
|
316
|
+
}): Promise<T[K]> {
|
|
317
|
+
return this.relationLoader.loadRelationByName<T, K>(
|
|
318
|
+
params.entity as T,
|
|
319
|
+
params.entityClass,
|
|
320
|
+
params.relationName,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Save entity with relations
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* const saved = await db.saveWithRelations({
|
|
329
|
+
* entity: user,
|
|
330
|
+
* entityClass: User,
|
|
331
|
+
* });
|
|
332
|
+
*/
|
|
333
|
+
async saveWithRelations<T extends BaseEntity>(params: {
|
|
334
|
+
entity: T;
|
|
335
|
+
entityClass: EntityConstructor<T>;
|
|
336
|
+
}): Promise<T> {
|
|
337
|
+
return this.relationLoader.saveWithRelations(params.entity, params.entityClass);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Delete entity with cascade handling for relations
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* await db.deleteWithRelations({
|
|
345
|
+
* entity: user,
|
|
346
|
+
* entityClass: User,
|
|
347
|
+
* });
|
|
348
|
+
*/
|
|
349
|
+
async deleteWithRelations<T extends BaseEntity>(params: {
|
|
350
|
+
entity: T;
|
|
351
|
+
entityClass: EntityConstructor<T>;
|
|
352
|
+
}): Promise<void> {
|
|
353
|
+
return this.relationLoader
|
|
354
|
+
.deleteWithRelations(params.entity, params.entityClass);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Manual sync with cloud
|
|
359
|
+
*/
|
|
360
|
+
async sync(): Promise<void> {
|
|
361
|
+
return this.cloudSyncService.sync();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get sync status
|
|
366
|
+
*/
|
|
367
|
+
getSyncStatus(): { enabled: boolean; lastSync?: Date; isOnline?: boolean } {
|
|
368
|
+
return this.cloudSyncService.getSyncStatus();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Enable cloud sync (if not already enabled)
|
|
373
|
+
*/
|
|
374
|
+
async enableCloudSync(config: CloudSyncConfig): Promise<void> {
|
|
375
|
+
return this.cloudSyncService.enableCloudSync(config);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Disable cloud sync
|
|
380
|
+
*/
|
|
381
|
+
disableCloudSync(): void {
|
|
382
|
+
this.cloudSyncService.disableCloudSync();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if cloud sync is enabled
|
|
387
|
+
*/
|
|
388
|
+
isCloudSyncEnabled(): boolean {
|
|
389
|
+
return this.cloudSyncService.isCloudSyncEnabled();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get cloud sync configuration
|
|
394
|
+
*/
|
|
395
|
+
getCloudSyncConfig(): CloudSyncConfig | undefined {
|
|
396
|
+
return this.cloudSyncService.getCloudSyncConfig();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Force sync specific tables
|
|
401
|
+
*/
|
|
402
|
+
async syncTables(tableNames: string[]): Promise<void> {
|
|
403
|
+
return this.cloudSyncService.syncTables(tableNames);
|
|
404
|
+
}
|
|
405
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Database } from './Database';
|
|
2
|
+
export { ValidationError } from './errors/ValidationError';
|
|
3
|
+
export {
|
|
4
|
+
Entity, ManyToMany,
|
|
5
|
+
OneToMany, OneToOne, Relation,
|
|
6
|
+
} from './metadata';
|
|
7
|
+
export { BaseEntity } from './services/BaseEntity';
|
|
8
|
+
export { newEntity } from './services/EntityFactory';
|
|
9
|
+
export { defineEntity } from './services/EntityRegistry';
|
|
10
|
+
export { EntitySchema } from './services/EntitySchema';
|
|
11
|
+
export type {
|
|
12
|
+
CloudSyncConfig, DatabaseConfig,EntityConstructor, EntityInstance,
|
|
13
|
+
} from './types';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
|
|
2
|
+
import {
|
|
3
|
+
getDefinedColumns,
|
|
4
|
+
getDefinedCompoundIndexes,
|
|
5
|
+
} from '../services/EntityRegistry';
|
|
6
|
+
import type {
|
|
7
|
+
ColumnOptions, CompoundIndexOptions,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
const COLUMNS_METADATA_KEY = Symbol('columns');
|
|
11
|
+
const COMPOUND_INDEXES_METADATA_KEY = Symbol('compoundIndexes');
|
|
12
|
+
|
|
13
|
+
export interface ColumnMetadata extends ColumnOptions {
|
|
14
|
+
propertyKey: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getColumnMetadata(
|
|
18
|
+
target: object | ClassConstructor,
|
|
19
|
+
): Record<string, ColumnMetadata> {
|
|
20
|
+
const ctor: ClassConstructor =
|
|
21
|
+
(typeof target === 'function')
|
|
22
|
+
? (target as ClassConstructor)
|
|
23
|
+
: (target as { constructor: ClassConstructor }).constructor;
|
|
24
|
+
const defined = getDefinedColumns(ctor as ClassConstructor);
|
|
25
|
+
|
|
26
|
+
if (defined) {
|
|
27
|
+
const out: Record<string, ColumnMetadata> = {};
|
|
28
|
+
|
|
29
|
+
for (const [key, cfg] of Object.entries(defined)) {
|
|
30
|
+
out[key] = { propertyKey: key, ...cfg } as ColumnMetadata;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasPrimary = Object.values(out).some(c => c.primaryKey);
|
|
34
|
+
if (!hasPrimary) {
|
|
35
|
+
if (!out.id) {
|
|
36
|
+
out.id = { propertyKey: 'id', primaryKey: true, autoIncrement: true } as ColumnMetadata;
|
|
37
|
+
} else {
|
|
38
|
+
out.id.primaryKey = true;
|
|
39
|
+
if (out.id.autoIncrement === undefined) {
|
|
40
|
+
out.id.autoIncrement = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const reflectGet = (Reflect as any)?.getMetadata?.bind(Reflect);
|
|
50
|
+
|
|
51
|
+
return reflectGet ? (reflectGet(COLUMNS_METADATA_KEY, ctor) || {}) : {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getCompoundIndexMetadata(
|
|
55
|
+
target: object | ClassConstructor,
|
|
56
|
+
): CompoundIndexOptions[] {
|
|
57
|
+
const ctor: ClassConstructor =
|
|
58
|
+
(typeof target === 'function')
|
|
59
|
+
? (target as ClassConstructor)
|
|
60
|
+
: (target as { constructor: ClassConstructor }).constructor;
|
|
61
|
+
const defined = getDefinedCompoundIndexes(ctor as ClassConstructor);
|
|
62
|
+
|
|
63
|
+
if (defined) {
|
|
64
|
+
return defined
|
|
65
|
+
.map(ci => ({ name: ci.name, unique: ci.unique, columns: ci.columns }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
const reflectGet = (Reflect as any)?.getMetadata?.bind(Reflect);
|
|
70
|
+
|
|
71
|
+
return reflectGet ? (reflectGet(COMPOUND_INDEXES_METADATA_KEY, ctor) || []) : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
|
|
4
|
+
import { getDefinedEntityMeta } from '../services/EntityRegistry';
|
|
5
|
+
import type { EntityOptions } from '../types';
|
|
6
|
+
|
|
7
|
+
const ENTITY_METADATA_KEY = Symbol('entity');
|
|
8
|
+
|
|
9
|
+
export interface EntityMetadata {
|
|
10
|
+
tableName: string;
|
|
11
|
+
schema?: z.ZodSchema<unknown>;
|
|
12
|
+
timestamps?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Entity(_options: EntityOptions = {}):
|
|
16
|
+
(_target: ClassConstructor) => void {
|
|
17
|
+
return function(_target: ClassConstructor): void {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getEntityMetadata(target: ClassConstructor):
|
|
21
|
+
EntityMetadata | undefined {
|
|
22
|
+
const defined = getDefinedEntityMeta(target);
|
|
23
|
+
|
|
24
|
+
if (defined) {
|
|
25
|
+
return {
|
|
26
|
+
tableName: defined.tableName,
|
|
27
|
+
schema: defined.schema,
|
|
28
|
+
timestamps: defined.timestamps,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const reflectGet = (Reflect as any)?.getMetadata?.bind(Reflect);
|
|
34
|
+
|
|
35
|
+
if (reflectGet) {
|
|
36
|
+
return reflectGet(ENTITY_METADATA_KEY, target);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
type ClassConstructor<T = unknown> = abstract new (..._args: never[]) => T;
|
|
2
|
+
type LegacyArgs = [Record<'constructor', ClassConstructor>, string];
|
|
3
|
+
type FieldContext = {
|
|
4
|
+
kind: 'field';
|
|
5
|
+
name: string;
|
|
6
|
+
addInitializer(
|
|
7
|
+
_init: (_this: { constructor: ClassConstructor }) => void
|
|
8
|
+
): void;
|
|
9
|
+
};
|
|
10
|
+
type StandardArgs = [unknown, FieldContext];
|
|
11
|
+
function isLegacyArgs(args: unknown[]): args is LegacyArgs {
|
|
12
|
+
return Array.isArray(args) && typeof (args as unknown[])[1] === 'string';
|
|
13
|
+
}
|
|
14
|
+
function isFieldContext(args: unknown[]): args is StandardArgs {
|
|
15
|
+
const ctx = (args as unknown[])[1] as FieldContext | undefined;
|
|
16
|
+
|
|
17
|
+
return !!ctx && typeof ctx === 'object' && ctx.kind === 'field' && typeof ctx.name === 'string';
|
|
18
|
+
}
|
|
19
|
+
import { getDefinedRelations } from '../services/EntityRegistry';
|
|
20
|
+
import type {
|
|
21
|
+
EntityConstructor,RelationMetadata, RelationOptions,
|
|
22
|
+
} from '../types';
|
|
23
|
+
|
|
24
|
+
const RELATIONS_METADATA_KEY = Symbol('relations');
|
|
25
|
+
|
|
26
|
+
export function Relation(options: RelationOptions): (..._args: unknown[]) => void {
|
|
27
|
+
return function (...args: unknown[]): void {
|
|
28
|
+
const apply = (ctor: ClassConstructor, propertyKey: string) => {
|
|
29
|
+
const existingRelations = getRelationMetadata(ctor) || {};
|
|
30
|
+
const relationMetadata: RelationMetadata = { propertyKey, ...options };
|
|
31
|
+
|
|
32
|
+
existingRelations[propertyKey] = relationMetadata;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
const reflectDefine = (Reflect as any)?.defineMetadata?.bind(Reflect);
|
|
35
|
+
|
|
36
|
+
if (reflectDefine) {
|
|
37
|
+
reflectDefine(RELATIONS_METADATA_KEY, existingRelations, ctor);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (isLegacyArgs(args)) {
|
|
42
|
+
const [target, propertyKey] = args;
|
|
43
|
+
|
|
44
|
+
apply(target.constructor, propertyKey);
|
|
45
|
+
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isFieldContext(args)) {
|
|
50
|
+
const [, context] = args;
|
|
51
|
+
const propertyKey = context.name;
|
|
52
|
+
|
|
53
|
+
context.addInitializer(function (_this: { constructor: ClassConstructor }) {
|
|
54
|
+
apply(_this.constructor, propertyKey);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getRelationMetadata(
|
|
61
|
+
entityClass: ClassConstructor,
|
|
62
|
+
): Record<string, RelationMetadata> | undefined {
|
|
63
|
+
const defined = getDefinedRelations(entityClass);
|
|
64
|
+
|
|
65
|
+
if (defined) {
|
|
66
|
+
const out: Record<string, RelationMetadata> = {};
|
|
67
|
+
for (const [key, rel] of Object.entries(defined)) {
|
|
68
|
+
out[key] = { propertyKey: key, ...rel } as RelationMetadata;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
const reflectGet = (Reflect as any)?.getMetadata?.bind(Reflect);
|
|
76
|
+
|
|
77
|
+
return reflectGet
|
|
78
|
+
? (reflectGet(RELATIONS_METADATA_KEY, entityClass) || {})
|
|
79
|
+
: {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function OneToOne(params: {
|
|
83
|
+
target: EntityConstructor | string;
|
|
84
|
+
foreignKey?: string;
|
|
85
|
+
options?: Partial<RelationOptions>;
|
|
86
|
+
}): (..._args: unknown[]) => void {
|
|
87
|
+
return Relation({
|
|
88
|
+
type: 'one-to-one',
|
|
89
|
+
target: params.target,
|
|
90
|
+
foreignKey: params.foreignKey,
|
|
91
|
+
...params.options,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function OneToMany(params: {
|
|
96
|
+
target: EntityConstructor | string;
|
|
97
|
+
foreignKey: string;
|
|
98
|
+
options?: Partial<RelationOptions>;
|
|
99
|
+
}): (..._args: unknown[]) => void {
|
|
100
|
+
return Relation({
|
|
101
|
+
type: 'one-to-many',
|
|
102
|
+
target: params.target,
|
|
103
|
+
foreignKey: params.foreignKey,
|
|
104
|
+
...params.options,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function ManyToMany(params: {
|
|
109
|
+
target: EntityConstructor | string;
|
|
110
|
+
joinTable: string;
|
|
111
|
+
options?: Partial<RelationOptions>;
|
|
112
|
+
}): (..._args: unknown[]) => void {
|
|
113
|
+
return Relation({
|
|
114
|
+
type: 'many-to-many',
|
|
115
|
+
target: params.target,
|
|
116
|
+
joinTable: params.joinTable,
|
|
117
|
+
...params.options,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|