remult 0.25.8 → 0.26.0
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/README.md +194 -129
- package/esm/graphql.js +131 -59
- package/esm/index.js +1 -1
- package/esm/migrations/compare-migration-snapshots.js +61 -0
- package/esm/migrations/generate-migrations.js +68 -0
- package/esm/migrations/index.js +2 -0
- package/esm/migrations/migrate.js +90 -0
- package/esm/migrations/migration-types.js +36 -0
- package/esm/migrations/update-migrations-ts-file.js +115 -0
- package/esm/postgres/postgres-data-provider.js +24 -3
- package/esm/postgres/schema-builder.js +35 -26
- package/esm/remult-better-sqlite3.js +47 -0
- package/esm/remult-bun-sqlite.js +41 -0
- package/esm/remult-knex/index.js +180 -51
- package/esm/remult-mongo.js +2 -2
- package/esm/remult-sql-js.js +3 -78
- package/esm/remult-sqlite-core.js +104 -0
- package/esm/server/expressBridge.js +130 -29
- package/esm/server/initAsyncHooks.js +75 -1
- package/esm/src/CompoundIdField.js +8 -2
- package/esm/src/context.js +20 -24
- package/esm/src/data-api.js +71 -30
- package/esm/src/data-providers/rest-data-provider.js +17 -15
- package/esm/src/data-providers/sql-database.js +32 -7
- package/esm/src/data-providers/web-sql-data-provider.js +1 -9
- package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +37 -11
- package/esm/src/filter/filter-interfaces.js +229 -17
- package/esm/src/isOfType.js +9 -0
- package/esm/src/remult3/Fields.js +70 -18
- package/esm/src/remult3/RepositoryImplementation.js +47 -21
- package/esm/src/remult3/addValidator.js +15 -0
- package/esm/src/remult3/remult3.js +28 -57
- package/esm/src/server-action.js +4 -4
- package/esm/src/validators.js +6 -3
- package/esm/src/valueConverters.js +1 -1
- package/graphql.js +156 -71
- package/index.d.ts +4 -4
- package/index.js +2 -1
- package/migrations/compare-migration-snapshots.d.ts +27 -0
- package/migrations/compare-migration-snapshots.js +165 -0
- package/migrations/generate-migrations.d.ts +21 -0
- package/migrations/generate-migrations.js +104 -0
- package/migrations/index.d.ts +3 -0
- package/migrations/index.js +7 -0
- package/migrations/migrate.d.ts +27 -0
- package/migrations/migrate.js +175 -0
- package/migrations/migration-types.d.ts +48 -0
- package/migrations/migration-types.js +88 -0
- package/migrations/update-migrations-ts-file.d.ts +1 -0
- package/migrations/update-migrations-ts-file.js +141 -0
- package/package.json +21 -1
- package/postgres/postgres-data-provider.d.ts +8 -3
- package/postgres/postgres-data-provider.js +46 -3
- package/postgres/schema-builder.js +81 -48
- package/remult-better-sqlite3.d.ts +12 -0
- package/remult-better-sqlite3.js +64 -0
- package/remult-bun-sqlite.d.ts +11 -0
- package/remult-bun-sqlite.js +56 -0
- package/remult-knex/index.d.ts +12 -5
- package/remult-knex/index.js +257 -90
- package/remult-mongo.d.ts +2 -2
- package/remult-mongo.js +2 -2
- package/remult-sql-js.d.ts +2 -15
- package/remult-sql-js.js +11 -152
- package/remult-sqlite-core.d.ts +21 -0
- package/remult-sqlite-core.js +217 -0
- package/server/expressBridge.d.ts +30 -1
- package/server/expressBridge.js +140 -32
- package/server/initAsyncHooks.d.ts +16 -0
- package/server/initAsyncHooks.js +128 -2
- package/src/CompoundIdField.js +8 -2
- package/src/column-interfaces.d.ts +9 -3
- package/src/context.d.ts +8 -6
- package/src/context.js +27 -36
- package/src/data-api.d.ts +13 -9
- package/src/data-api.js +184 -135
- package/src/data-providers/rest-data-provider.js +26 -15
- package/src/data-providers/sql-database.d.ts +17 -5
- package/src/data-providers/sql-database.js +31 -7
- package/src/data-providers/web-sql-data-provider.d.ts +1 -2
- package/src/data-providers/web-sql-data-provider.js +4 -8
- package/src/entity.d.ts +64 -6
- package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +31 -7
- package/src/filter/filter-consumer-bridge-to-sql-request.js +46 -21
- package/src/filter/filter-interfaces.d.ts +106 -39
- package/src/filter/filter-interfaces.js +312 -32
- package/src/isOfType.d.ts +2 -0
- package/src/isOfType.js +14 -0
- package/src/remult3/Fields.d.ts +36 -0
- package/src/remult3/Fields.js +81 -19
- package/src/remult3/RepositoryImplementation.d.ts +7 -3
- package/src/remult3/RepositoryImplementation.js +106 -71
- package/src/remult3/addValidator.d.ts +2 -0
- package/src/remult3/addValidator.js +20 -0
- package/src/remult3/remult3.d.ts +6 -2
- package/src/remult3/remult3.js +28 -57
- package/src/server-action.d.ts +1 -1
- package/src/server-action.js +5 -4
- package/src/sql-command.d.ts +6 -1
- package/src/validators.d.ts +1 -0
- package/src/validators.js +10 -4
- package/src/valueConverters.js +1 -1
package/esm/graphql.js
CHANGED
|
@@ -118,6 +118,11 @@ export function remultGraphql(options) {
|
|
|
118
118
|
for (const meta of entities) {
|
|
119
119
|
const orderByFields = [];
|
|
120
120
|
const key = meta.key;
|
|
121
|
+
const requiredWhereArg = {
|
|
122
|
+
key: 'where',
|
|
123
|
+
value: `${key}Where!`,
|
|
124
|
+
comment: `Remult filtering options`,
|
|
125
|
+
};
|
|
121
126
|
const currentType = upsertTypes(getMetaType(meta), 'type_impl_node');
|
|
122
127
|
if (key) {
|
|
123
128
|
const createResultPromise = (work) => {
|
|
@@ -213,6 +218,7 @@ export function remultGraphql(options) {
|
|
|
213
218
|
});
|
|
214
219
|
};
|
|
215
220
|
const queryArgsConnection = getQueryArgsConnection(key);
|
|
221
|
+
let pluralEntityKey = key;
|
|
216
222
|
let getSingleEntityKey = toCamelCase(getMetaType(meta));
|
|
217
223
|
if (getSingleEntityKey === toCamelCase(key))
|
|
218
224
|
getSingleEntityKey = 'single' + key;
|
|
@@ -325,66 +331,126 @@ export function remultGraphql(options) {
|
|
|
325
331
|
if (checkCanExist(meta.options.allowApiUpdate)) {
|
|
326
332
|
// update
|
|
327
333
|
const updateInput = `Update${getMetaType(meta)}Input`;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
334
|
+
{
|
|
335
|
+
const updatePayload = `Update${getMetaType(meta)}Payload`;
|
|
336
|
+
const updateResolverKey = `update${getMetaType(meta)}`;
|
|
337
|
+
root_mutation.fields.push({
|
|
338
|
+
key: updateResolverKey,
|
|
339
|
+
args: [
|
|
340
|
+
argId,
|
|
341
|
+
{ key: 'patch', value: `${updateInput}!` },
|
|
342
|
+
argClientMutationId,
|
|
343
|
+
],
|
|
344
|
+
value: `${updatePayload}`,
|
|
345
|
+
comment: `Update a \`${getMetaType(meta)}\``,
|
|
346
|
+
});
|
|
347
|
+
currentType.mutation.update.input = upsertTypes(updateInput, 'input');
|
|
348
|
+
currentType.mutation.update.payload = upsertTypes(updatePayload);
|
|
349
|
+
currentType.mutation.update.payload.fields.push({
|
|
350
|
+
key: `${toCamelCase(getMetaType(meta))}`,
|
|
351
|
+
value: `${getMetaType(meta)}`,
|
|
352
|
+
}, argErrorDetail, argClientMutationId);
|
|
353
|
+
resolversMutation[updateResolverKey] = (origItem, args, req, gqlInfo) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
|
|
354
|
+
await dApi.put({
|
|
355
|
+
...response,
|
|
356
|
+
success: (y) => {
|
|
357
|
+
const orig = { ...y };
|
|
358
|
+
currentType.query.resultProcessors.forEach((z) => z(y, orig));
|
|
359
|
+
setResult({
|
|
360
|
+
[toCamelCase(getMetaType(meta))]: y,
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
}, arg1.id, arg1.patch);
|
|
364
|
+
})(args, req);
|
|
365
|
+
}
|
|
366
|
+
// update many
|
|
367
|
+
{
|
|
368
|
+
const updateManyPayload = `UpdateMany${upperFirstChar(pluralEntityKey)}Payload`;
|
|
369
|
+
const updateManyResolverKey = `updateMany${upperFirstChar(pluralEntityKey)}`;
|
|
370
|
+
root_mutation.fields.push({
|
|
371
|
+
key: updateManyResolverKey,
|
|
372
|
+
args: [
|
|
373
|
+
requiredWhereArg,
|
|
374
|
+
{ key: 'patch', value: `${updateInput}!` },
|
|
375
|
+
argClientMutationId,
|
|
376
|
+
],
|
|
377
|
+
value: `${updateManyPayload}`,
|
|
378
|
+
comment: `Update many \`${pluralEntityKey}\``,
|
|
379
|
+
});
|
|
380
|
+
currentType.mutation.update.payload = upsertTypes(updateManyPayload);
|
|
381
|
+
currentType.mutation.update.payload.fields.push({
|
|
382
|
+
key: `updated`,
|
|
383
|
+
value: `Int!`,
|
|
384
|
+
}, argErrorDetail, argClientMutationId);
|
|
385
|
+
resolversMutation[updateManyResolverKey] = (origItem, args, req, gqlInfo) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
|
|
386
|
+
await dApi.updateManyImplementation({
|
|
387
|
+
...response,
|
|
388
|
+
success: (updateManyResult) => {
|
|
389
|
+
setResult(updateManyResult);
|
|
390
|
+
},
|
|
391
|
+
}, { get: () => undefined }, {
|
|
392
|
+
...translateWhereToRestBody(meta.fields, arg1),
|
|
393
|
+
set: arg1.patch,
|
|
394
|
+
});
|
|
395
|
+
})(args, req);
|
|
396
|
+
}
|
|
359
397
|
}
|
|
360
398
|
if (checkCanExist(meta.options.allowApiDelete)) {
|
|
361
399
|
// delete
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
{
|
|
401
|
+
const deletePayload = `Delete${getMetaType(meta)}Payload`;
|
|
402
|
+
const deleteResolverKey = `delete${getMetaType(meta)}`;
|
|
403
|
+
root_mutation.fields.push({
|
|
404
|
+
key: deleteResolverKey,
|
|
405
|
+
args: [argId, argClientMutationId],
|
|
406
|
+
value: `${deletePayload}`,
|
|
407
|
+
comment: `Delete a \`${getMetaType(meta)}\``,
|
|
408
|
+
});
|
|
409
|
+
currentType.mutation.delete.payload = upsertTypes(deletePayload);
|
|
410
|
+
const deletedResultKey = `id`;
|
|
411
|
+
const type = meta.idMetadata.field instanceof CompoundIdField
|
|
412
|
+
? 'String'
|
|
413
|
+
: getGraphqlBaseType(meta.idMetadata.field);
|
|
414
|
+
currentType.mutation.delete.payload.fields.push({
|
|
415
|
+
key: deletedResultKey,
|
|
416
|
+
value: `${type}`,
|
|
417
|
+
}, argErrorDetail, argClientMutationId);
|
|
418
|
+
resolversMutation[deleteResolverKey] = (origItem, args, req) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
|
|
419
|
+
await dApi.delete({
|
|
420
|
+
...response,
|
|
421
|
+
deleted: () => {
|
|
422
|
+
setResult({ [deletedResultKey]: arg1.id });
|
|
423
|
+
},
|
|
424
|
+
}, arg1.id);
|
|
425
|
+
})(args, req);
|
|
426
|
+
}
|
|
427
|
+
// delete
|
|
428
|
+
{
|
|
429
|
+
const deleteManyPayload = `DeleteMany${upperFirstChar(pluralEntityKey)}Payload`;
|
|
430
|
+
const deleteManyResolverKey = `deleteMany${upperFirstChar(pluralEntityKey)}`;
|
|
431
|
+
root_mutation.fields.push({
|
|
432
|
+
key: deleteManyResolverKey,
|
|
433
|
+
args: [requiredWhereArg, argClientMutationId],
|
|
434
|
+
value: `${deleteManyPayload}`,
|
|
435
|
+
comment: `Delete many \`${pluralEntityKey}\``,
|
|
436
|
+
});
|
|
437
|
+
currentType.mutation.delete.payload = upsertTypes(deleteManyPayload);
|
|
438
|
+
const type = meta.idMetadata.field instanceof CompoundIdField
|
|
439
|
+
? 'String'
|
|
440
|
+
: getGraphqlBaseType(meta.idMetadata.field);
|
|
441
|
+
currentType.mutation.delete.payload.fields.push({
|
|
442
|
+
key: 'deleted',
|
|
443
|
+
value: `Int!`,
|
|
444
|
+
}, argErrorDetail, argClientMutationId);
|
|
445
|
+
resolversMutation[deleteManyResolverKey] = (origItem, args, req) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
|
|
446
|
+
await dApi.deleteMany({
|
|
447
|
+
...response,
|
|
448
|
+
success: (deleteManyResult) => {
|
|
449
|
+
setResult(deleteManyResult);
|
|
450
|
+
},
|
|
451
|
+
}, { get: () => undefined }, translateWhereToRestBody(meta.fields, arg1));
|
|
452
|
+
})(args, req);
|
|
453
|
+
}
|
|
388
454
|
}
|
|
389
455
|
const whereTypeFields = [];
|
|
390
456
|
for (const f of meta.fields) {
|
|
@@ -838,6 +904,12 @@ function getGraphqlBaseType(field) {
|
|
|
838
904
|
function getMetaType(entityMeta) {
|
|
839
905
|
return entityMeta.entityType.name;
|
|
840
906
|
}
|
|
907
|
+
function upperFirstChar(str) {
|
|
908
|
+
return str
|
|
909
|
+
.split('')
|
|
910
|
+
.map((c, i) => (i === 0 ? c.toUpperCase() : c))
|
|
911
|
+
.join('');
|
|
912
|
+
}
|
|
841
913
|
function toCamelCase(str) {
|
|
842
914
|
return str
|
|
843
915
|
.split('')
|
|
@@ -904,7 +976,7 @@ export function translateWhereToRestBody(fields, { where }) {
|
|
|
904
976
|
}
|
|
905
977
|
}
|
|
906
978
|
if (where.OR) {
|
|
907
|
-
result.OR = where.OR.map((where) => translateWhereToRestBody(fields, { where }));
|
|
979
|
+
result.OR = where.OR.map((where) => translateWhereToRestBody(fields, { where }).where);
|
|
908
980
|
}
|
|
909
|
-
return result;
|
|
981
|
+
return { where: result };
|
|
910
982
|
}
|
package/esm/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { CustomSqlFilterBuilder, dbNamesOf, } from './src/filter/filter-consumer
|
|
|
13
13
|
export { JsonDataProvider, } from './src/data-providers/json-data-provider.js'; //V
|
|
14
14
|
export { JsonEntityOpfsStorage } from './src/data-providers/json-entity-opfs-storage.js';
|
|
15
15
|
//export * from './src/data-api'; //reconsider if to make internal
|
|
16
|
-
export { Controller, BackendMethod, ProgressListener, } from './src/server-action.js';
|
|
16
|
+
export { Controller, BackendMethod, ProgressListener, ForbiddenError, } from './src/server-action.js';
|
|
17
17
|
export { Allow, Remult, withRemult, isBackend, EventSource, } from './src/context.js';
|
|
18
18
|
export { Sort } from './src/sort.js';
|
|
19
19
|
export { CompoundIdField } from './src/CompoundIdField.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { repo } from '../index.js';
|
|
2
|
+
import { dbNamesOf, shouldCreateEntity, shouldNotCreateField, } from '../src/filter/filter-consumer-bridge-to-sql-request.js';
|
|
3
|
+
export async function compareMigrationSnapshot({ entities, snapshot, migrationBuilder, reporter, }) {
|
|
4
|
+
snapshot = JSON.parse(JSON.stringify(snapshot));
|
|
5
|
+
let processedEntities = new Set();
|
|
6
|
+
for (const entity of entities) {
|
|
7
|
+
const meta = repo(entity).metadata;
|
|
8
|
+
const e = await dbNamesOf(meta, (x) => x);
|
|
9
|
+
processedEntities.add(e.$entityName);
|
|
10
|
+
if (shouldCreateEntity(meta, e)) {
|
|
11
|
+
let entitySnapshot = snapshot.entities[e.$entityName];
|
|
12
|
+
let createColumns = true;
|
|
13
|
+
if (!entitySnapshot) {
|
|
14
|
+
createColumns = false;
|
|
15
|
+
reporter('create table ' + e.$entityName);
|
|
16
|
+
await migrationBuilder.createTable(meta);
|
|
17
|
+
entitySnapshot = snapshot.entities[e.$entityName] = {
|
|
18
|
+
key: meta.key,
|
|
19
|
+
className: meta.entityType.name,
|
|
20
|
+
columns: {},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const processedColumns = new Set();
|
|
24
|
+
for (const field of meta.fields) {
|
|
25
|
+
if (!shouldNotCreateField(field, e)) {
|
|
26
|
+
const columnDbName = e.$dbNameOf(field);
|
|
27
|
+
processedColumns.add(columnDbName);
|
|
28
|
+
const column = entitySnapshot.columns[columnDbName];
|
|
29
|
+
if (!column) {
|
|
30
|
+
if (createColumns) {
|
|
31
|
+
reporter('add column ' + e.$entityName + '.' + columnDbName);
|
|
32
|
+
await migrationBuilder.addColumn(meta, field);
|
|
33
|
+
}
|
|
34
|
+
entitySnapshot.columns[columnDbName] = { key: field.key };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const columnDbName of Object.keys(entitySnapshot.columns)) {
|
|
39
|
+
if (!processedColumns.has(columnDbName)) {
|
|
40
|
+
reporter('remove column ' + e.$entityName + '.' + columnDbName);
|
|
41
|
+
await migrationBuilder.removeColumn(e.$entityName, columnDbName);
|
|
42
|
+
delete entitySnapshot.columns[columnDbName];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const entityDbName of Object.keys(snapshot.entities)) {
|
|
48
|
+
if (!processedEntities.has(entityDbName)) {
|
|
49
|
+
reporter('remove table ' + entityDbName);
|
|
50
|
+
await migrationBuilder.removeTable(entityDbName);
|
|
51
|
+
delete snapshot.entities[entityDbName];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return snapshot;
|
|
55
|
+
}
|
|
56
|
+
export function emptySnapshot() {
|
|
57
|
+
return {
|
|
58
|
+
version: 1,
|
|
59
|
+
entities: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { compareMigrationSnapshot, emptySnapshot, } from './compare-migration-snapshots.js';
|
|
4
|
+
import { updateMigrationsFile } from './update-migrations-ts-file.js';
|
|
5
|
+
import { DefaultMigrationBuilder, } from './migration-types.js';
|
|
6
|
+
import { isOfType } from '../src/isOfType.js';
|
|
7
|
+
import { initDataProvider } from '../server/initDataProvider.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates migration scripts based on changes in entities.
|
|
10
|
+
*
|
|
11
|
+
* @param options - Configuration options for generating migrations.
|
|
12
|
+
* @param options.entities - An array of entity classes whose changes will be included in the migration.
|
|
13
|
+
* @param options.dataProvider - The data provider instance or a function returning a promise of the data provider.
|
|
14
|
+
* @param options.migrationsFolder - (Optional) The path to the folder where migration scripts will be stored. Default is 'src/migrations'.
|
|
15
|
+
* @param options.snapshotFile - (Optional) The path to the file where the snapshot of the last known state will be stored. Default is 'migrations-snapshot.json' in the `migrationsFolder`.
|
|
16
|
+
* @param options.migrationsTSFile - (Optional) The path to the TypeScript file where the generated migrations will be written. Default is 'migrations.ts' in the `migrationsFolder`.
|
|
17
|
+
* @param options.endConnection - (Optional) Determines whether to close the database connection after generating migrations. Default is false.
|
|
18
|
+
* @see [Migrations](https://remult.dev/docs/migrations.html)
|
|
19
|
+
*/
|
|
20
|
+
export async function generateMigrations(options) {
|
|
21
|
+
const migrationDir = options.migrationsFolder || path.join(process.cwd(), 'src/migrations');
|
|
22
|
+
const snapshotFileName = options.snapshotFile || path.join(migrationDir, 'migrations-snapshot.json');
|
|
23
|
+
const migrationsTSFilename = options.migrationsTSFile || path.join(migrationDir, 'migrations.ts');
|
|
24
|
+
const dataProvider = await initDataProvider(options.dataProvider);
|
|
25
|
+
for (const p of [snapshotFileName, migrationsTSFilename]) {
|
|
26
|
+
if (!fs.existsSync(path.dirname(p))) {
|
|
27
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let snapshot = emptySnapshot();
|
|
31
|
+
if (fs.existsSync(snapshotFileName)) {
|
|
32
|
+
snapshot = JSON.parse(fs.readFileSync(snapshotFileName).toString());
|
|
33
|
+
}
|
|
34
|
+
let hasSql = false;
|
|
35
|
+
let steps = [];
|
|
36
|
+
const code = {
|
|
37
|
+
//https://marketplace.visualstudio.com/items?itemName=qufiwefefwoyn.inline-sql-syntax
|
|
38
|
+
addSql: (sql) => {
|
|
39
|
+
steps.push('await sql(`--sql\n' + sql + '`)');
|
|
40
|
+
hasSql = true;
|
|
41
|
+
},
|
|
42
|
+
addTypescriptCode: (code) => steps.push(code),
|
|
43
|
+
addComment: (comment) => steps.push(comment.indexOf('\n') >= 0 ? `/*${comment}*/` : `// ${comment}`),
|
|
44
|
+
};
|
|
45
|
+
let migrationBuilder = new DefaultMigrationBuilder(code);
|
|
46
|
+
if (isOfType(dataProvider, 'provideMigrationBuilder')) {
|
|
47
|
+
migrationBuilder = new DefaultMigrationBuilder(code, dataProvider.provideMigrationBuilder(code));
|
|
48
|
+
}
|
|
49
|
+
let reporter = (what) => console.log('[generateMigrations] ' + what);
|
|
50
|
+
snapshot = await compareMigrationSnapshot({
|
|
51
|
+
entities: options.entities,
|
|
52
|
+
snapshot,
|
|
53
|
+
reporter,
|
|
54
|
+
migrationBuilder,
|
|
55
|
+
});
|
|
56
|
+
if (steps.length)
|
|
57
|
+
await updateMigrationsFile(migrationsTSFilename, [
|
|
58
|
+
`async (${hasSql ? '{sql}' : ''})=>{\n${steps.join('\n')}\n}`,
|
|
59
|
+
]);
|
|
60
|
+
else {
|
|
61
|
+
reporter('No changes detected');
|
|
62
|
+
}
|
|
63
|
+
fs.writeFileSync(snapshotFileName, JSON.stringify(snapshot, null, 2));
|
|
64
|
+
if (options.endConnection && isOfType(dataProvider, 'end')) {
|
|
65
|
+
await dataProvider.end();
|
|
66
|
+
}
|
|
67
|
+
return steps.length > 0;
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { Entity, IdEntity, Fields, remult, Remult, } from '../index.js';
|
|
3
|
+
import { initDataProvider } from '../server/initDataProvider.js';
|
|
4
|
+
import { doTransaction } from '../src/context.js';
|
|
5
|
+
import { cast, isOfType } from '../src/isOfType.js';
|
|
6
|
+
/**
|
|
7
|
+
* Applies migration scripts to update the database schema.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Configuration options for applying migrations.
|
|
10
|
+
* @param options.migrations - An object containing the migration scripts, each keyed by a unique identifier.
|
|
11
|
+
* @param options.dataProvider - The data provider instance or a function returning a promise of the data provider.
|
|
12
|
+
* @param options.migrationsTable - (Optional) The name of the table that tracks applied migrations. Default is '__remult_migrations_version'.
|
|
13
|
+
* @param options.endConnection - (Optional) Determines whether to close the database connection after applying migrations. Default is false.
|
|
14
|
+
* @param options.beforeMigration - (Optional) A callback function that is called before each migration is applied. Receives an object with the migration index.
|
|
15
|
+
* @param options.afterMigration - (Optional) A callback function that is called after each migration is applied. Receives an object with the migration index and the duration of the migration.
|
|
16
|
+
* @see [Migrations](https://remult.dev/docs/migrations.html)
|
|
17
|
+
*/
|
|
18
|
+
export async function migrate(options) {
|
|
19
|
+
let migrationTableName = options.migrationsTable || '__remult_migrations_version';
|
|
20
|
+
const dataProvider = await initDataProvider(options.dataProvider);
|
|
21
|
+
const prev = remult.dataProvider;
|
|
22
|
+
remult.dataProvider = dataProvider;
|
|
23
|
+
try {
|
|
24
|
+
let VersionInfo = class VersionInfo extends IdEntity {
|
|
25
|
+
version = -1;
|
|
26
|
+
};
|
|
27
|
+
__decorate([
|
|
28
|
+
Fields.number(),
|
|
29
|
+
__metadata("design:type", Object)
|
|
30
|
+
], VersionInfo.prototype, "version", void 0);
|
|
31
|
+
VersionInfo = __decorate([
|
|
32
|
+
Entity(migrationTableName)
|
|
33
|
+
], VersionInfo);
|
|
34
|
+
const steps = Object.entries(options.migrations).sort(([a], [b]) => parseInt(a) - parseInt(b));
|
|
35
|
+
if (dataProvider.ensureSchema) {
|
|
36
|
+
await dataProvider.ensureSchema([
|
|
37
|
+
new Remult(dataProvider).repo(VersionInfo).metadata,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
await doTransaction(remult, async (dp) => {
|
|
41
|
+
const repo = new Remult(dataProvider).repo(VersionInfo, dp);
|
|
42
|
+
let v = await repo.findFirst();
|
|
43
|
+
if (!v) {
|
|
44
|
+
v = repo.create();
|
|
45
|
+
v.version = -1;
|
|
46
|
+
}
|
|
47
|
+
for (const [stepText, action] of steps) {
|
|
48
|
+
const step = parseInt(stepText);
|
|
49
|
+
if (step < 0)
|
|
50
|
+
throw new Error('Migration step number must be a non-negative integer');
|
|
51
|
+
if (v.version >= step)
|
|
52
|
+
continue;
|
|
53
|
+
if (options.beforeMigration) {
|
|
54
|
+
await options.beforeMigration({ index: step });
|
|
55
|
+
}
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
console.info('Executing migration step ' + step);
|
|
58
|
+
console.time('Completed migration step ' + step);
|
|
59
|
+
try {
|
|
60
|
+
const utils = {
|
|
61
|
+
sql: async (sql) => {
|
|
62
|
+
return await cast(dp, 'execute').execute(sql);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
await action(utils);
|
|
66
|
+
if (options.afterMigration) {
|
|
67
|
+
await options.afterMigration({
|
|
68
|
+
index: step,
|
|
69
|
+
duration: Date.now() - startTime,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
console.timeEnd('Completed migration step ' + step);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error('Failed to execute migration step ' + step);
|
|
76
|
+
console.error(err);
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
v.version = step;
|
|
80
|
+
await v.save();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
remult.dataProvider = prev;
|
|
86
|
+
}
|
|
87
|
+
if (options.endConnection && isOfType(dataProvider, 'end')) {
|
|
88
|
+
await dataProvider.end();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class DefaultMigrationBuilder {
|
|
2
|
+
code;
|
|
3
|
+
wrapped;
|
|
4
|
+
constructor(code, wrapped) {
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.wrapped = wrapped;
|
|
7
|
+
}
|
|
8
|
+
async createTable(meta) {
|
|
9
|
+
if (this.wrapped?.createTable) {
|
|
10
|
+
await this.wrapped.createTable(meta);
|
|
11
|
+
}
|
|
12
|
+
else
|
|
13
|
+
this.code.addComment('TODO: implement create table ' + meta.dbName);
|
|
14
|
+
}
|
|
15
|
+
async addColumn(meta, field) {
|
|
16
|
+
if (this.wrapped?.addColumn) {
|
|
17
|
+
await this.wrapped.addColumn(meta, field);
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
this.code.addComment('TODO: implement add column ' + meta.dbName + '.' + field.dbName);
|
|
21
|
+
}
|
|
22
|
+
async removeTable(entityDbName) {
|
|
23
|
+
if (this.wrapped?.removeTable) {
|
|
24
|
+
await this.wrapped.removeTable(entityDbName);
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
this.code.addComment('TODO: implement remove table ' + entityDbName);
|
|
28
|
+
}
|
|
29
|
+
async removeColumn(entityName, columnDbName) {
|
|
30
|
+
if (this.wrapped?.removeColumn) {
|
|
31
|
+
await this.wrapped.removeColumn(entityName, columnDbName);
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
this.code.addComment('TODO: implement remove column ' + entityName + '.' + columnDbName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// consider replacing with https://www.npmjs.com/package/ts-morph
|
|
2
|
+
export async function updateMigrationsFile(filePath, steps) {
|
|
3
|
+
let ts = await import('typescript');
|
|
4
|
+
//@ts-ignore
|
|
5
|
+
if (ts.default)
|
|
6
|
+
ts = ts.default;
|
|
7
|
+
const fs = await import('fs');
|
|
8
|
+
let maxStep = -1;
|
|
9
|
+
function buildSteps() {
|
|
10
|
+
return steps.map((step) => ts.factory.createPropertyAssignment(ts.factory.createIdentifier((++maxStep).toString()), ts.factory.createIdentifier(step)));
|
|
11
|
+
}
|
|
12
|
+
if (!fs.existsSync(filePath)) {
|
|
13
|
+
fs.writeFileSync(filePath, '');
|
|
14
|
+
}
|
|
15
|
+
const importPath = 'remult/migrations';
|
|
16
|
+
const importName = 'Migrations';
|
|
17
|
+
// Load the file
|
|
18
|
+
const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(), ts.ScriptTarget.Latest, true);
|
|
19
|
+
let migrationsExportFound = false;
|
|
20
|
+
// Find the "migrations" named export and add the new member
|
|
21
|
+
let updatedSourceFile = ts.transform(sourceFile, [
|
|
22
|
+
(context) => {
|
|
23
|
+
return (node) => {
|
|
24
|
+
const visitor = (node) => {
|
|
25
|
+
if (ts.isVariableStatement(node) &&
|
|
26
|
+
node.declarationList.declarations.length > 0) {
|
|
27
|
+
const declaration = node.declarationList.declarations[0];
|
|
28
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
29
|
+
declaration.name.text === 'migrations' &&
|
|
30
|
+
declaration.initializer &&
|
|
31
|
+
ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
32
|
+
migrationsExportFound = true;
|
|
33
|
+
declaration.initializer.properties.forEach((property) => {
|
|
34
|
+
if (ts.isPropertyAssignment(property)) {
|
|
35
|
+
if (ts.isNumericLiteral(property.name)) {
|
|
36
|
+
const step = parseInt(property.name.text);
|
|
37
|
+
if (step > maxStep) {
|
|
38
|
+
maxStep = step;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
let steps = buildSteps();
|
|
44
|
+
// Found the "migrations" array
|
|
45
|
+
// Update the initializer with the new steps
|
|
46
|
+
const updatedInitializer = ts.factory.updateObjectLiteralExpression(declaration.initializer, [...declaration.initializer.properties, ...steps]);
|
|
47
|
+
// Update the declaration with the new initializer
|
|
48
|
+
const updatedDeclaration = ts.factory.updateVariableDeclaration(declaration, declaration.name, undefined, declaration.type, // Preserve the original type annotation if present
|
|
49
|
+
updatedInitializer);
|
|
50
|
+
return ts.factory.updateVariableStatement(node, node.modifiers, ts.factory.updateVariableDeclarationList(node.declarationList, [
|
|
51
|
+
updatedDeclaration,
|
|
52
|
+
]));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return ts.visitEachChild(node, visitor, context);
|
|
56
|
+
};
|
|
57
|
+
return ts.visitNode(node, visitor);
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
]).transformed[0];
|
|
61
|
+
if (!migrationsExportFound) {
|
|
62
|
+
let importFound = false;
|
|
63
|
+
// Check if the import statement already exists
|
|
64
|
+
sourceFile.statements.forEach((statement) => {
|
|
65
|
+
if (ts.isImportDeclaration(statement) &&
|
|
66
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
67
|
+
statement.moduleSpecifier.text === `${importPath}`) {
|
|
68
|
+
statement.importClause?.namedBindings?.forEachChild((namedBinding) => {
|
|
69
|
+
if (ts.isImportSpecifier(namedBinding) &&
|
|
70
|
+
namedBinding.name.text === importName) {
|
|
71
|
+
importFound = true;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// If the import statement doesn't exist, create it
|
|
77
|
+
if (!importFound) {
|
|
78
|
+
updatedSourceFile = ts.factory.updateSourceFile(updatedSourceFile, [
|
|
79
|
+
ts.factory.createExpressionStatement(ts.factory.createIdentifier(`import type { ${importName} } from '${importPath}'`)),
|
|
80
|
+
...updatedSourceFile.statements,
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
// Create a new export for migrations if it doesn't exist
|
|
84
|
+
const newMigrationsExport = ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
|
|
85
|
+
ts.factory.createVariableDeclaration('migrations', undefined, // You can add a type annotation here if needed
|
|
86
|
+
ts.factory.createTypeReferenceNode(importName, undefined), ts.factory.createObjectLiteralExpression(buildSteps(), false)),
|
|
87
|
+
], ts.NodeFlags.Const));
|
|
88
|
+
// Add the new export to the end of the file
|
|
89
|
+
updatedSourceFile = ts.factory.updateSourceFile(updatedSourceFile, [
|
|
90
|
+
...updatedSourceFile.statements,
|
|
91
|
+
newMigrationsExport,
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
let code = ts.createPrinter().printFile(updatedSourceFile);
|
|
95
|
+
try {
|
|
96
|
+
let prettier;
|
|
97
|
+
try {
|
|
98
|
+
prettier = await import('prettier' + '');
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.warn('error importing prettier,for better formatting run `npm i -D prettier`. Writing raw code to file.');
|
|
102
|
+
}
|
|
103
|
+
if (prettier) {
|
|
104
|
+
let options = await prettier.resolveConfig();
|
|
105
|
+
code = await prettier.format(code, {
|
|
106
|
+
...options,
|
|
107
|
+
parser: 'typescript',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.log('error formatting code, writing raw code to file.');
|
|
113
|
+
}
|
|
114
|
+
fs.writeFileSync(filePath, code);
|
|
115
|
+
}
|