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.
Files changed (102) hide show
  1. package/README.md +194 -129
  2. package/esm/graphql.js +131 -59
  3. package/esm/index.js +1 -1
  4. package/esm/migrations/compare-migration-snapshots.js +61 -0
  5. package/esm/migrations/generate-migrations.js +68 -0
  6. package/esm/migrations/index.js +2 -0
  7. package/esm/migrations/migrate.js +90 -0
  8. package/esm/migrations/migration-types.js +36 -0
  9. package/esm/migrations/update-migrations-ts-file.js +115 -0
  10. package/esm/postgres/postgres-data-provider.js +24 -3
  11. package/esm/postgres/schema-builder.js +35 -26
  12. package/esm/remult-better-sqlite3.js +47 -0
  13. package/esm/remult-bun-sqlite.js +41 -0
  14. package/esm/remult-knex/index.js +180 -51
  15. package/esm/remult-mongo.js +2 -2
  16. package/esm/remult-sql-js.js +3 -78
  17. package/esm/remult-sqlite-core.js +104 -0
  18. package/esm/server/expressBridge.js +130 -29
  19. package/esm/server/initAsyncHooks.js +75 -1
  20. package/esm/src/CompoundIdField.js +8 -2
  21. package/esm/src/context.js +20 -24
  22. package/esm/src/data-api.js +71 -30
  23. package/esm/src/data-providers/rest-data-provider.js +17 -15
  24. package/esm/src/data-providers/sql-database.js +32 -7
  25. package/esm/src/data-providers/web-sql-data-provider.js +1 -9
  26. package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +37 -11
  27. package/esm/src/filter/filter-interfaces.js +229 -17
  28. package/esm/src/isOfType.js +9 -0
  29. package/esm/src/remult3/Fields.js +70 -18
  30. package/esm/src/remult3/RepositoryImplementation.js +47 -21
  31. package/esm/src/remult3/addValidator.js +15 -0
  32. package/esm/src/remult3/remult3.js +28 -57
  33. package/esm/src/server-action.js +4 -4
  34. package/esm/src/validators.js +6 -3
  35. package/esm/src/valueConverters.js +1 -1
  36. package/graphql.js +156 -71
  37. package/index.d.ts +4 -4
  38. package/index.js +2 -1
  39. package/migrations/compare-migration-snapshots.d.ts +27 -0
  40. package/migrations/compare-migration-snapshots.js +165 -0
  41. package/migrations/generate-migrations.d.ts +21 -0
  42. package/migrations/generate-migrations.js +104 -0
  43. package/migrations/index.d.ts +3 -0
  44. package/migrations/index.js +7 -0
  45. package/migrations/migrate.d.ts +27 -0
  46. package/migrations/migrate.js +175 -0
  47. package/migrations/migration-types.d.ts +48 -0
  48. package/migrations/migration-types.js +88 -0
  49. package/migrations/update-migrations-ts-file.d.ts +1 -0
  50. package/migrations/update-migrations-ts-file.js +141 -0
  51. package/package.json +21 -1
  52. package/postgres/postgres-data-provider.d.ts +8 -3
  53. package/postgres/postgres-data-provider.js +46 -3
  54. package/postgres/schema-builder.js +81 -48
  55. package/remult-better-sqlite3.d.ts +12 -0
  56. package/remult-better-sqlite3.js +64 -0
  57. package/remult-bun-sqlite.d.ts +11 -0
  58. package/remult-bun-sqlite.js +56 -0
  59. package/remult-knex/index.d.ts +12 -5
  60. package/remult-knex/index.js +257 -90
  61. package/remult-mongo.d.ts +2 -2
  62. package/remult-mongo.js +2 -2
  63. package/remult-sql-js.d.ts +2 -15
  64. package/remult-sql-js.js +11 -152
  65. package/remult-sqlite-core.d.ts +21 -0
  66. package/remult-sqlite-core.js +217 -0
  67. package/server/expressBridge.d.ts +30 -1
  68. package/server/expressBridge.js +140 -32
  69. package/server/initAsyncHooks.d.ts +16 -0
  70. package/server/initAsyncHooks.js +128 -2
  71. package/src/CompoundIdField.js +8 -2
  72. package/src/column-interfaces.d.ts +9 -3
  73. package/src/context.d.ts +8 -6
  74. package/src/context.js +27 -36
  75. package/src/data-api.d.ts +13 -9
  76. package/src/data-api.js +184 -135
  77. package/src/data-providers/rest-data-provider.js +26 -15
  78. package/src/data-providers/sql-database.d.ts +17 -5
  79. package/src/data-providers/sql-database.js +31 -7
  80. package/src/data-providers/web-sql-data-provider.d.ts +1 -2
  81. package/src/data-providers/web-sql-data-provider.js +4 -8
  82. package/src/entity.d.ts +64 -6
  83. package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +31 -7
  84. package/src/filter/filter-consumer-bridge-to-sql-request.js +46 -21
  85. package/src/filter/filter-interfaces.d.ts +106 -39
  86. package/src/filter/filter-interfaces.js +312 -32
  87. package/src/isOfType.d.ts +2 -0
  88. package/src/isOfType.js +14 -0
  89. package/src/remult3/Fields.d.ts +36 -0
  90. package/src/remult3/Fields.js +81 -19
  91. package/src/remult3/RepositoryImplementation.d.ts +7 -3
  92. package/src/remult3/RepositoryImplementation.js +106 -71
  93. package/src/remult3/addValidator.d.ts +2 -0
  94. package/src/remult3/addValidator.js +20 -0
  95. package/src/remult3/remult3.d.ts +6 -2
  96. package/src/remult3/remult3.js +28 -57
  97. package/src/server-action.d.ts +1 -1
  98. package/src/server-action.js +5 -4
  99. package/src/sql-command.d.ts +6 -1
  100. package/src/validators.d.ts +1 -0
  101. package/src/validators.js +10 -4
  102. 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
- const updatePayload = `Update${getMetaType(meta)}Payload`;
329
- const updateResolverKey = `update${getMetaType(meta)}`;
330
- root_mutation.fields.push({
331
- key: updateResolverKey,
332
- args: [
333
- argId,
334
- { key: 'patch', value: `${updateInput}!` },
335
- argClientMutationId,
336
- ],
337
- value: `${updatePayload}`,
338
- comment: `Update a \`${getMetaType(meta)}\``,
339
- });
340
- currentType.mutation.update.input = upsertTypes(updateInput, 'input');
341
- currentType.mutation.update.payload = upsertTypes(updatePayload);
342
- currentType.mutation.update.payload.fields.push({
343
- key: `${toCamelCase(getMetaType(meta))}`,
344
- value: `${getMetaType(meta)}`,
345
- }, argErrorDetail, argClientMutationId);
346
- root[updateResolverKey] = handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
347
- await dApi.put({
348
- ...response,
349
- success: (y) => {
350
- const orig = { ...y };
351
- currentType.query.resultProcessors.forEach((z) => z(y, orig));
352
- setResult({
353
- [toCamelCase(getMetaType(meta))]: y,
354
- });
355
- },
356
- }, arg1.id, arg1.patch);
357
- });
358
- resolversMutation[updateResolverKey] = (origItem, args, req, gqlInfo) => root[updateResolverKey](args, req, gqlInfo);
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
- const deletePayload = `Delete${getMetaType(meta)}Payload`;
363
- const deleteResolverKey = `delete${getMetaType(meta)}`;
364
- root_mutation.fields.push({
365
- key: deleteResolverKey,
366
- args: [argId, argClientMutationId],
367
- value: `${deletePayload}`,
368
- comment: `Delete a \`${getMetaType(meta)}\``,
369
- });
370
- currentType.mutation.delete.payload = upsertTypes(deletePayload);
371
- const deletedResultKey = `id`;
372
- const type = meta.idMetadata.field instanceof CompoundIdField
373
- ? 'String'
374
- : getGraphqlBaseType(meta.idMetadata.field);
375
- currentType.mutation.delete.payload.fields.push({
376
- key: deletedResultKey,
377
- value: `${type}`,
378
- }, argErrorDetail, argClientMutationId);
379
- root[deleteResolverKey] = handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
380
- await dApi.delete({
381
- ...response,
382
- deleted: () => {
383
- setResult({ [deletedResultKey]: arg1.id });
384
- },
385
- }, arg1.id);
386
- });
387
- resolversMutation[deleteResolverKey] = (origItem, args, req, gqlInfo) => root[deleteResolverKey](args, req, gqlInfo);
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,2 @@
1
+ export { generateMigrations } from './generate-migrations.js';
2
+ export { migrate } from './migrate.js';
@@ -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
+ }