stabilize-orm 1.0.8 → 1.0.9
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/.eslintrc.json +10 -0
- package/.github/workflows/ci-cd.yml +73 -0
- package/bun.lock +600 -0
- package/cli/stabilize-cli.ts +594 -0
- package/lib/LICENSE +21 -0
- package/lib/cache.ts +110 -0
- package/lib/client.ts +258 -0
- package/lib/decorators.ts +99 -0
- package/lib/index.ts +143 -0
- package/lib/logger.ts +126 -0
- package/lib/migrations.ts +81 -0
- package/lib/query-builder.ts +96 -0
- package/lib/repository.ts +565 -0
- package/lib/types.ts +76 -0
- package/package.json +3 -15
- package/tests/migrations.test.ts +141 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { generateMigration, runMigrations } from '../lib/migrations';
|
|
3
|
+
import { DBType } from '../lib/types';
|
|
4
|
+
// We must mock the imports that provide the metadata keys and the client implementation
|
|
5
|
+
import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from '../lib/decorators';
|
|
6
|
+
import { DBClient } from '../lib/client';
|
|
7
|
+
|
|
8
|
+
// --- MOCKING SETUP ---
|
|
9
|
+
|
|
10
|
+
// Mocking Reflect.getMetadata behavior to simulate data set by the @Model, @Column, etc. decorators
|
|
11
|
+
const mockMetadata = new Map();
|
|
12
|
+
|
|
13
|
+
// Helper to set mock data for tests
|
|
14
|
+
const setMockMetadata = (tableName: string, columns: any, validators: any, softDeleteField?: string) => {
|
|
15
|
+
mockMetadata.set(ModelKey, tableName);
|
|
16
|
+
mockMetadata.set(ColumnKey, columns);
|
|
17
|
+
mockMetadata.set(ValidatorKey, validators);
|
|
18
|
+
if (softDeleteField) {
|
|
19
|
+
mockMetadata.set(SoftDeleteKey, softDeleteField);
|
|
20
|
+
} else {
|
|
21
|
+
mockMetadata.delete(SoftDeleteKey);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Spy on the global Reflect.getMetadata used by the functions to return our mock data
|
|
26
|
+
const getMetadataSpy = vi.spyOn(Reflect, 'getMetadata');
|
|
27
|
+
getMetadataSpy.mockImplementation((key, target) => mockMetadata.get(key));
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// Mock Model Placeholder
|
|
31
|
+
class MockModel {}
|
|
32
|
+
|
|
33
|
+
// Mock DBClient for runMigrations test: this prevents hitting a real database
|
|
34
|
+
vi.mock('./client', () => {
|
|
35
|
+
const mockQuery = vi.fn(async (query: string) => {
|
|
36
|
+
// Simulate no existing migration found when selecting from 'migrations'
|
|
37
|
+
if (query.includes('SELECT id FROM migrations')) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
DBClient: vi.fn(() => ({
|
|
44
|
+
query: mockQuery,
|
|
45
|
+
close: vi.fn(async () => {}),
|
|
46
|
+
// Provide a mock config for runMigrations to safely access DBType
|
|
47
|
+
config: {
|
|
48
|
+
type: DBType.SQLite
|
|
49
|
+
}
|
|
50
|
+
}))
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// --- TESTS START HERE ---
|
|
55
|
+
|
|
56
|
+
describe('generateMigration', () => {
|
|
57
|
+
const commonColumns = {
|
|
58
|
+
id: { name: 'id', type: 'INTEGER' },
|
|
59
|
+
username: { name: 'user_name', type: 'TEXT' },
|
|
60
|
+
createdAt: { name: 'created_at', type: 'TEXT' }
|
|
61
|
+
};
|
|
62
|
+
const commonValidators = {
|
|
63
|
+
username: ['required', 'unique']
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
it('should generate SQLite-specific primary key (AUTOINCREMENT)', async () => {
|
|
67
|
+
setMockMetadata('users', commonColumns, commonValidators);
|
|
68
|
+
|
|
69
|
+
const migration = await generateMigration(MockModel, DBType.SQLite);
|
|
70
|
+
|
|
71
|
+
expect(migration.up[0]).toBe(
|
|
72
|
+
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL UNIQUE, created_at TEXT)'
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should generate PostgreSQL-specific primary key (SERIAL)', async () => {
|
|
77
|
+
setMockMetadata('products', commonColumns, commonValidators);
|
|
78
|
+
|
|
79
|
+
const migration = await generateMigration(MockModel, DBType.Postgres);
|
|
80
|
+
|
|
81
|
+
expect(migration.up[0]).toBe(
|
|
82
|
+
'CREATE TABLE IF NOT EXISTS products (id SERIAL PRIMARY KEY, user_name TEXT NOT NULL UNIQUE, created_at TEXT)'
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should generate MySQL-specific primary key (AUTO_INCREMENT)', async () => {
|
|
87
|
+
setMockMetadata('posts', commonColumns, commonValidators);
|
|
88
|
+
|
|
89
|
+
const migration = await generateMigration(MockModel, DBType.MySQL);
|
|
90
|
+
|
|
91
|
+
expect(migration.up[0]).toBe(
|
|
92
|
+
'CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTO_INCREMENT, user_name TEXT NOT NULL UNIQUE, created_at TEXT)'
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should include soft delete field if present on the model', async () => {
|
|
97
|
+
const softDeleteColumns = {
|
|
98
|
+
...commonColumns,
|
|
99
|
+
deletedAt: { name: 'deleted_at', type: 'TEXT' }
|
|
100
|
+
};
|
|
101
|
+
setMockMetadata('orders', softDeleteColumns, commonValidators, 'deletedAt');
|
|
102
|
+
|
|
103
|
+
const migration = await generateMigration(MockModel, DBType.SQLite);
|
|
104
|
+
|
|
105
|
+
expect(migration.up[0]).toContain(', deleted_at TEXT)');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw an error if the model is missing the @Model decorator', async () => {
|
|
109
|
+
mockMetadata.delete(ModelKey);
|
|
110
|
+
await expect(generateMigration(MockModel, DBType.SQLite)).rejects.toThrow('Model not decorated with @Model');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('runMigrations', () => {
|
|
115
|
+
it('should create the migrations table and run the UP script for new migrations', async () => {
|
|
116
|
+
const mockMigrations = [
|
|
117
|
+
{ up: ['CREATE TABLE test_table (id INT)'], down: ['DROP TABLE test_table'] }
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Run migrations using the mocked DBClient (defaulted to SQLite type)
|
|
121
|
+
await runMigrations({ type: DBType.SQLite,connectionString:"" }, mockMigrations);
|
|
122
|
+
|
|
123
|
+
// Access the mock instance
|
|
124
|
+
const mockClient = (DBClient as any).mock.results[0].value;
|
|
125
|
+
|
|
126
|
+
// 1. Verify CREATE TABLE IF NOT EXISTS migrations was called
|
|
127
|
+
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS migrations ('));
|
|
128
|
+
|
|
129
|
+
// 2. Verify the actual UP query was executed
|
|
130
|
+
expect(mockClient.query).toHaveBeenCalledWith('CREATE TABLE test_table (id INT)', []);
|
|
131
|
+
|
|
132
|
+
// 3. Verify the migration log record was inserted
|
|
133
|
+
expect(mockClient.query).toHaveBeenCalledWith(
|
|
134
|
+
expect.stringContaining('INSERT INTO migrations (name, applied_at) VALUES (?, ?)'),
|
|
135
|
+
expect.arrayContaining([expect.stringContaining('migration_0_'), expect.any(String)])
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// 4. Verify the client was closed in the finally block
|
|
139
|
+
expect(mockClient.close).toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
28
|
+
|
|
29
|
+
"experimentalDecorators": true,
|
|
30
|
+
"emitDecoratorMetadata": true
|
|
31
|
+
}
|
|
32
|
+
}
|