revisium 2.0.1 → 2.2.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 (97) hide show
  1. package/.github/workflows/ci.yml +32 -0
  2. package/README.md +23 -3
  3. package/dist/e2e/setup/global-setup.js +2 -3
  4. package/dist/e2e/setup/global-setup.js.map +1 -1
  5. package/dist/e2e/setup/global-teardown.js +0 -2
  6. package/dist/e2e/setup/global-teardown.js.map +1 -1
  7. package/dist/e2e/utils/constants.d.ts +0 -2
  8. package/dist/e2e/utils/constants.js +0 -2
  9. package/dist/e2e/utils/constants.js.map +1 -1
  10. package/dist/e2e/utils/docker-helper.d.ts +0 -2
  11. package/dist/e2e/utils/docker-helper.js +0 -13
  12. package/dist/e2e/utils/docker-helper.js.map +1 -1
  13. package/dist/package.json +27 -14
  14. package/dist/src/commands/migration/apply-migrations.command.d.ts +1 -1
  15. package/dist/src/commands/migration/apply-migrations.command.js +4 -5
  16. package/dist/src/commands/migration/apply-migrations.command.js.map +1 -1
  17. package/dist/src/commands/migration/save-migrations.command.d.ts +1 -1
  18. package/dist/src/commands/migration/save-migrations.command.js +6 -6
  19. package/dist/src/commands/migration/save-migrations.command.js.map +1 -1
  20. package/dist/src/commands/rows/save-rows.command.d.ts +1 -1
  21. package/dist/src/commands/rows/save-rows.command.js +12 -10
  22. package/dist/src/commands/rows/save-rows.command.js.map +1 -1
  23. package/dist/src/commands/rows/upload-rows.command.d.ts +1 -1
  24. package/dist/src/commands/rows/upload-rows.command.js +12 -15
  25. package/dist/src/commands/rows/upload-rows.command.js.map +1 -1
  26. package/dist/src/commands/schema/create-migrations.command.js.map +1 -1
  27. package/dist/src/commands/schema/save-schema.command.d.ts +1 -1
  28. package/dist/src/commands/schema/save-schema.command.js +7 -7
  29. package/dist/src/commands/schema/save-schema.command.js.map +1 -1
  30. package/dist/src/config/meta-schema.d.ts +3 -0
  31. package/dist/src/config/meta-schema.js +43 -1
  32. package/dist/src/config/meta-schema.js.map +1 -1
  33. package/dist/src/services/connection/api-client-adapter.d.ts +2 -4
  34. package/dist/src/services/connection/api-client-adapter.js +40 -36
  35. package/dist/src/services/connection/api-client-adapter.js.map +1 -1
  36. package/dist/src/services/connection/api-client.d.ts +3 -4
  37. package/dist/src/services/connection/api-client.js +11 -33
  38. package/dist/src/services/connection/api-client.js.map +1 -1
  39. package/dist/src/services/connection/connection-factory.service.d.ts +4 -6
  40. package/dist/src/services/connection/connection-factory.service.js +18 -39
  41. package/dist/src/services/connection/connection-factory.service.js.map +1 -1
  42. package/dist/src/services/connection/connection.service.d.ts +2 -73
  43. package/dist/src/services/connection/connection.service.js +2 -11
  44. package/dist/src/services/connection/connection.service.js.map +1 -1
  45. package/dist/src/services/sync/commit-revision.service.js +6 -28
  46. package/dist/src/services/sync/commit-revision.service.js.map +1 -1
  47. package/dist/src/services/sync/row-sync.service.d.ts +5 -5
  48. package/dist/src/services/sync/row-sync.service.js +10 -10
  49. package/dist/src/services/sync/row-sync.service.js.map +1 -1
  50. package/dist/src/services/sync/sync-data.service.d.ts +1 -0
  51. package/dist/src/services/sync/sync-data.service.js +21 -21
  52. package/dist/src/services/sync/sync-data.service.js.map +1 -1
  53. package/dist/src/services/sync/sync-schema.service.d.ts +1 -0
  54. package/dist/src/services/sync/sync-schema.service.js +11 -10
  55. package/dist/src/services/sync/sync-schema.service.js.map +1 -1
  56. package/dist/src/services/url/auth-prompt.service.js +1 -1
  57. package/dist/src/services/url/auth-prompt.service.js.map +1 -1
  58. package/dist/src/types/migration.types.d.ts +1 -1
  59. package/dist/tsconfig.build.tsbuildinfo +1 -1
  60. package/docs/authentication.md +3 -3
  61. package/docs/configuration.md +58 -9
  62. package/docs/docker-deployment.md +48 -13
  63. package/docs/migrate-commands.md +35 -10
  64. package/docs/rows-commands.md +30 -7
  65. package/docs/schema-commands.md +21 -5
  66. package/docs/sync-commands.md +44 -12
  67. package/docs/url-format.md +2 -2
  68. package/e2e/setup/global-setup.ts +3 -9
  69. package/e2e/setup/global-teardown.ts +0 -6
  70. package/e2e/utils/constants.ts +0 -2
  71. package/e2e/utils/docker-helper.ts +0 -23
  72. package/package.json +27 -14
  73. package/src/commands/migration/apply-migrations.command.ts +5 -6
  74. package/src/commands/migration/save-migrations.command.ts +7 -6
  75. package/src/commands/rows/save-rows.command.ts +14 -28
  76. package/src/commands/rows/upload-rows.command.ts +7 -15
  77. package/src/commands/schema/create-migrations.command.ts +1 -1
  78. package/src/commands/schema/save-schema.command.ts +9 -14
  79. package/src/config/meta-schema.ts +47 -0
  80. package/src/services/connection/__tests__/connection-factory.service.spec.ts +117 -0
  81. package/src/services/connection/__tests__/connection.service.spec.ts +27 -117
  82. package/src/services/connection/api-client-adapter.ts +41 -45
  83. package/src/services/connection/api-client.ts +11 -50
  84. package/src/services/connection/connection-factory.service.ts +35 -65
  85. package/src/services/connection/connection.service.ts +3 -14
  86. package/src/services/sync/__tests__/row-sync.service.spec.ts +3 -6
  87. package/src/services/sync/commit-revision.service.ts +7 -51
  88. package/src/services/sync/row-sync.service.ts +4 -18
  89. package/src/services/sync/sync-data.service.ts +32 -45
  90. package/src/services/sync/sync-schema.service.ts +14 -22
  91. package/src/services/url/auth-prompt.service.ts +1 -1
  92. package/src/types/migration.types.ts +2 -2
  93. package/dist/src/__generated__/api.d.ts +0 -688
  94. package/dist/src/__generated__/api.js +0 -698
  95. package/dist/src/__generated__/api.js.map +0 -1
  96. package/e2e/docker-compose.e2e.yml +0 -31
  97. package/src/__generated__/api.ts +0 -2598
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revisium",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "homepage": "https://revisium.io",
5
5
  "description": "A CLI tool for interacting with Revisium instances, providing migration management, schema export, and data export capabilities.",
6
6
  "author": "Anton Kashirov",
@@ -27,13 +27,11 @@
27
27
  "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
28
28
  "test:e2e": "jest --config ./e2e/jest-e2e.json",
29
29
  "test:e2e:cov": "npm run build:instrumented && E2E_INSTRUMENTED=1 jest --config ./e2e/jest-e2e.json",
30
+ "test:e2e:up": "FILE_PLUGIN_PUBLIC_ENDPOINT=https://cdn.example.com npx @revisium/standalone --auth --port 8082 --data .e2e-test & echo $! > .e2e-test.pid",
31
+ "test:e2e:down": "if [ -f .e2e-test.pid ]; then kill $(cat .e2e-test.pid) 2>/dev/null; rm -f .e2e-test.pid; fi; rm -rf .e2e-test",
30
32
  "build:instrumented": "npm run build && rm -rf dist-instrumented && mkdir -p dist-instrumented && cp dist/package.json dist-instrumented/ && npx nyc instrument dist/src dist-instrumented/src --include='**/*.js'",
31
33
  "test:all": "npm run test:cov && npm run test:e2e:cov && npm run coverage:merge",
32
- "coverage:merge": "node scripts/merge-coverage.js",
33
- "docker:e2e:up": "docker-compose -f e2e/docker-compose.e2e.yml up -d",
34
- "docker:e2e:down": "docker-compose -f e2e/docker-compose.e2e.yml down -v",
35
- "docker:e2e:logs": "docker-compose -f e2e/docker-compose.e2e.yml logs -f",
36
- "generate:api": "npx swagger-typescript-api generate -p https://cloud.revisium.io/api-json -o src/__generated__ -n api.ts --extract-request-params --disable-throw-on-error"
34
+ "coverage:merge": "node scripts/merge-coverage.js"
37
35
  },
38
36
  "bin": {
39
37
  "revisium": "dist/src/main.js"
@@ -44,22 +42,24 @@
44
42
  "@nestjs/config": "^4.0.2",
45
43
  "@nestjs/core": "^11.0.1",
46
44
  "@nestjs/platform-express": "^11.0.1",
45
+ "@revisium/client": "^0.4.0",
47
46
  "@revisium/schema-toolkit": "^0.4.1",
48
47
  "@types/object-hash": "^3.0.6",
49
- "ajv": "^8.17.1",
48
+ "ajv": "^8.18.0",
50
49
  "ajv-formats": "^3.0.1",
51
50
  "nest-commander": "^3.18.0",
52
51
  "object-hash": "^3.0.0",
53
52
  "reflect-metadata": "^0.2.2",
54
- "rxjs": "^7.8.1",
55
- "swagger-typescript-api": "^13.2.7"
53
+ "rxjs": "^7.8.1"
56
54
  },
57
55
  "devDependencies": {
58
56
  "@eslint/eslintrc": "^3.2.0",
59
57
  "@eslint/js": "^9.18.0",
58
+ "@istanbuljs/nyc-config-typescript": "^1.0.2",
60
59
  "@nestjs/cli": "^11.0.0",
61
60
  "@nestjs/schematics": "^11.0.0",
62
61
  "@nestjs/testing": "^11.0.1",
62
+ "@revisium/standalone": "2.6.0",
63
63
  "@swc/cli": "^0.6.0",
64
64
  "@swc/core": "^1.10.7",
65
65
  "@types/express": "^5.0.0",
@@ -69,9 +69,10 @@
69
69
  "eslint": "^9.18.0",
70
70
  "eslint-config-prettier": "^10.0.1",
71
71
  "eslint-plugin-prettier": "^5.2.2",
72
- "eslint-plugin-sonarjs": "^3.0.5",
72
+ "eslint-plugin-sonarjs": "^4.0.2",
73
73
  "globals": "^16.0.0",
74
74
  "jest": "^29.7.0",
75
+ "nyc": "^17.1.0",
75
76
  "prettier": "^3.4.2",
76
77
  "source-map-support": "^0.5.21",
77
78
  "supertest": "^7.0.0",
@@ -80,9 +81,22 @@
80
81
  "ts-node": "^10.9.2",
81
82
  "tsconfig-paths": "^4.2.0",
82
83
  "typescript": "^5.7.3",
83
- "typescript-eslint": "^8.20.0",
84
- "nyc": "^17.1.0",
85
- "@istanbuljs/nyc-config-typescript": "^1.0.2"
84
+ "typescript-eslint": "^8.20.0"
85
+ },
86
+ "overrides": {
87
+ "hono": "^4.12.7",
88
+ "@hono/node-server": "^1.19.10",
89
+ "lodash": "^4.17.23",
90
+ "@angular-devkit/core": {
91
+ "ajv": "^8.18.0"
92
+ },
93
+ "schema-utils@3": {
94
+ "ajv": "^6.14.0"
95
+ },
96
+ "schema-utils@4": {
97
+ "ajv": "^8.18.0"
98
+ },
99
+ "file-type": "^21.3.2"
86
100
  },
87
101
  "jest": {
88
102
  "modulePaths": [
@@ -102,7 +116,6 @@
102
116
  "<rootDir>/src/**/*.(t|j)s",
103
117
  "!<rootDir>/src/main.ts",
104
118
  "!<rootDir>/src/app.module.ts",
105
- "!<rootDir>/src/__generated__/**",
106
119
  "!<rootDir>/src/**/__tests__/**",
107
120
  "!<rootDir>/src/**/index.ts"
108
121
  ],
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { Option, SubCommand } from 'nest-commander';
3
+ import { RevisionScope } from '@revisium/client';
3
4
  import { BaseCommand, BaseOptions } from 'src/commands/base.command';
4
5
  import { ConnectionService } from 'src/services/connection';
5
6
  import { JsonValidatorService, LoggerService } from 'src/services/common';
@@ -61,8 +62,6 @@ export class ApplyMigrationsCommand extends BaseCommand {
61
62
  }
62
63
 
63
64
  private async applyMigration(migrations: Migration[]) {
64
- const revisionId = this.connectionService.draftRevisionId;
65
-
66
65
  if (migrations.length === 0) {
67
66
  this.logger.success(
68
67
  'No migrations to apply - all migrations are up to date',
@@ -76,11 +75,11 @@ export class ApplyMigrationsCommand extends BaseCommand {
76
75
 
77
76
  try {
78
77
  for (const localMigration of migrations) {
79
- const result = await this.api.applyMigrations(revisionId, [
78
+ const results = await this.revisionScope.applyMigrationsWithStatus([
80
79
  localMigration,
81
80
  ]);
82
81
 
83
- const response = result.data[0];
82
+ const response = results[0];
84
83
 
85
84
  if (response.status === 'failed') {
86
85
  this.logger.migrationFailed(response);
@@ -114,8 +113,8 @@ export class ApplyMigrationsCommand extends BaseCommand {
114
113
  return countAppliedMigrations;
115
114
  }
116
115
 
117
- private get api() {
118
- return this.connectionService.api;
116
+ private get revisionScope(): RevisionScope {
117
+ return this.connectionService.revisionScope;
119
118
  }
120
119
 
121
120
  @Option({
@@ -1,5 +1,6 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import { Option, SubCommand } from 'nest-commander';
3
+ import { RevisionScope } from '@revisium/client';
3
4
  import { BaseCommand, BaseOptions } from 'src/commands/base.command';
4
5
  import { ConnectionService } from 'src/services/connection';
5
6
  import { LoggerService } from 'src/services/common';
@@ -26,14 +27,14 @@ export class SaveMigrationsCommand extends BaseCommand {
26
27
  }
27
28
 
28
29
  await this.connectionService.connect(options);
29
- await this.saveFile(this.connectionService.revisionId, options.file);
30
+ await this.saveFile(options.file);
30
31
  }
31
32
 
32
- private async saveFile(revisionId: string, filePath: string) {
33
+ private async saveFile(filePath: string) {
33
34
  try {
34
- const result = await this.api.migrations(revisionId);
35
+ const migrations = await this.revisionScope.getMigrations();
35
36
 
36
- await writeFile(filePath, JSON.stringify(result.data, null, 2), 'utf-8');
37
+ await writeFile(filePath, JSON.stringify(migrations, null, 2), 'utf-8');
37
38
 
38
39
  this.logger.success(`Save migrations to: ${filePath}`);
39
40
  } catch (error) {
@@ -53,7 +54,7 @@ export class SaveMigrationsCommand extends BaseCommand {
53
54
  return value;
54
55
  }
55
56
 
56
- private get api() {
57
- return this.connectionService.api;
57
+ private get revisionScope(): RevisionScope {
58
+ return this.connectionService.revisionScope;
58
59
  }
59
60
  }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { Option, SubCommand } from 'nest-commander';
4
+ import { RevisionScope } from '@revisium/client';
4
5
  import { BaseCommand, BaseOptions } from 'src/commands/base.command';
5
6
  import { ConnectionService } from 'src/services/connection';
6
7
  import { LoggerService } from 'src/services/common';
@@ -32,30 +33,19 @@ export class SaveRowsCommand extends BaseCommand {
32
33
  }
33
34
 
34
35
  await this.connectionService.connect(options);
35
- await this.saveAllTableRows(
36
- this.connectionService.revisionId,
37
- options.folder,
38
- options.tables,
39
- );
36
+ await this.saveAllTableRows(options.folder, options.tables);
40
37
  }
41
38
 
42
- private async saveAllTableRows(
43
- revisionId: string,
44
- folderPath: string,
45
- tableFilter?: string,
46
- ) {
39
+ private async saveAllTableRows(folderPath: string, tableFilter?: string) {
47
40
  try {
48
41
  await mkdir(folderPath, { recursive: true });
49
42
 
50
- const tablesToProcess = await this.getTargetTables(
51
- revisionId,
52
- tableFilter,
53
- );
43
+ const tablesToProcess = await this.getTargetTables(tableFilter);
54
44
 
55
45
  this.logger.foundItems(tablesToProcess.length, 'tables to process');
56
46
 
57
47
  for (const tableId of tablesToProcess) {
58
- await this.saveRowsFromTable(revisionId, tableId, folderPath);
48
+ await this.saveRowsFromTable(tableId, folderPath);
59
49
  }
60
50
 
61
51
  this.logger.summary(
@@ -69,26 +59,19 @@ export class SaveRowsCommand extends BaseCommand {
69
59
  }
70
60
  }
71
61
 
72
- private async getTargetTables(
73
- revisionId: string,
74
- tableFilter?: string,
75
- ): Promise<string[]> {
62
+ private async getTargetTables(tableFilter?: string): Promise<string[]> {
76
63
  if (tableFilter) {
77
64
  return tableFilter.split(',').map((id) => id.trim());
78
65
  }
79
66
 
80
67
  const { items } = await fetchAllPages((params) =>
81
- this.api.tables({ revisionId, ...params }),
68
+ this.revisionScope.getTables(params).then((data) => ({ data })),
82
69
  );
83
70
 
84
71
  return items.map((table) => table.id);
85
72
  }
86
73
 
87
- private async saveRowsFromTable(
88
- revisionId: string,
89
- tableId: string,
90
- folderPath: string,
91
- ) {
74
+ private async saveRowsFromTable(tableId: string, folderPath: string) {
92
75
  try {
93
76
  this.logger.processingTable(tableId);
94
77
 
@@ -96,7 +79,10 @@ export class SaveRowsCommand extends BaseCommand {
96
79
  await mkdir(tableFolderPath, { recursive: true });
97
80
 
98
81
  const { processed, total } = await fetchAndProcessPages(
99
- (params) => this.api.rows(revisionId, tableId, params),
82
+ (params) =>
83
+ this.revisionScope
84
+ .getRows(tableId, params)
85
+ .then((data) => ({ data })),
100
86
  async (row) => {
101
87
  const filePath = join(tableFolderPath, `${row.id}.json`);
102
88
  await writeFile(filePath, JSON.stringify(row, null, 2), 'utf-8');
@@ -119,8 +105,8 @@ export class SaveRowsCommand extends BaseCommand {
119
105
  }
120
106
  }
121
107
 
122
- private get api() {
123
- return this.connectionService.api;
108
+ private get revisionScope(): RevisionScope {
109
+ return this.connectionService.revisionScope;
124
110
  }
125
111
 
126
112
  @Option({
@@ -1,4 +1,5 @@
1
1
  import { Option, SubCommand } from 'nest-commander';
2
+ import { RevisionScope } from '@revisium/client';
2
3
  import { BaseCommand, BaseOptions } from 'src/commands/base.command';
3
4
  import {
4
5
  ConnectionService,
@@ -57,11 +58,9 @@ export class UploadRowsCommand extends BaseCommand {
57
58
  }
58
59
 
59
60
  await this.connectionService.connect(options);
60
- const revisionId = this.connectionService.draftRevisionId;
61
61
  const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
62
62
 
63
63
  const totalStats = await this.uploadAllTableRows(
64
- revisionId,
65
64
  options.folder,
66
65
  options.tables,
67
66
  batchSize,
@@ -76,7 +75,6 @@ export class UploadRowsCommand extends BaseCommand {
76
75
  }
77
76
 
78
77
  private async uploadAllTableRows(
79
- revisionId: string,
80
78
  folderPath: string,
81
79
  tableFilter: string | undefined,
82
80
  batchSize: number,
@@ -87,7 +85,7 @@ export class UploadRowsCommand extends BaseCommand {
87
85
  );
88
86
  this.logger.foundItems(tableIds.length, 'tables to process');
89
87
 
90
- const tableSchemas = await this.fetchTableSchemas(revisionId, tableIds);
88
+ const tableSchemas = await this.fetchTableSchemas(tableIds);
91
89
  const sortedTables = this.getSortedTables(tableSchemas, tableIds);
92
90
 
93
91
  const totalStats = createEmptyUploadStats();
@@ -95,7 +93,6 @@ export class UploadRowsCommand extends BaseCommand {
95
93
  for (const tableId of sortedTables) {
96
94
  try {
97
95
  const tableStats = await this.uploadTableRows(
98
- revisionId,
99
96
  tableId,
100
97
  folderPath,
101
98
  tableSchemas[tableId],
@@ -114,17 +111,14 @@ export class UploadRowsCommand extends BaseCommand {
114
111
  }
115
112
 
116
113
  private async fetchTableSchemas(
117
- revisionId: string,
118
114
  tables: string[],
119
115
  ): Promise<Record<string, JsonSchema>> {
120
116
  const schemas: Record<string, JsonSchema> = {};
121
117
 
122
118
  for (const tableId of tables) {
123
119
  try {
124
- const result = await this.api.tableSchema(revisionId, tableId);
125
- if (result.data) {
126
- schemas[tableId] = result.data as JsonSchema;
127
- }
120
+ const schema = await this.revisionScope.getTableSchema(tableId);
121
+ schemas[tableId] = schema as JsonSchema;
128
122
  } catch (error) {
129
123
  this.logger.warn(
130
124
  `Could not fetch schema for table ${tableId}: ${error instanceof Error ? error.message : String(error)}`,
@@ -153,7 +147,6 @@ export class UploadRowsCommand extends BaseCommand {
153
147
  }
154
148
 
155
149
  private async uploadTableRows(
156
- revisionId: string,
157
150
  tableId: string,
158
151
  folderPath: string,
159
152
  schema: JsonSchema | undefined,
@@ -190,10 +183,9 @@ export class UploadRowsCommand extends BaseCommand {
190
183
  return stats;
191
184
  }
192
185
 
193
- const apiClient = createApiClientAdapter(this.api);
186
+ const apiClient = createApiClientAdapter(this.revisionScope);
194
187
  const syncStats = await this.rowSyncService.syncTableRows(
195
188
  apiClient,
196
- revisionId,
197
189
  tableId,
198
190
  loadResult.rows,
199
191
  batchSize,
@@ -235,8 +227,8 @@ export class UploadRowsCommand extends BaseCommand {
235
227
  }
236
228
  }
237
229
 
238
- private get api() {
239
- return this.connectionService.api;
230
+ private get revisionScope(): RevisionScope {
231
+ return this.connectionService.revisionScope;
240
232
  }
241
233
 
242
234
  @Option({
@@ -1,10 +1,10 @@
1
1
  import { readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { join, extname } from 'node:path';
3
3
  import { CommandRunner, Option, SubCommand } from 'nest-commander';
4
+ import type { InitMigrationDto } from '@revisium/client';
4
5
  import { JsonValidatorService, LoggerService } from 'src/services/common';
5
6
  import { TableDependencyService } from 'src/services/sync';
6
7
  import { JsonSchema } from 'src/types/schema.types';
7
- import { InitMigrationDto } from 'src/__generated__/api';
8
8
  import * as objectHash from 'object-hash';
9
9
 
10
10
  type Options = {
@@ -1,6 +1,7 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { Option, SubCommand } from 'nest-commander';
4
+ import { RevisionScope } from '@revisium/client';
4
5
  import { BaseCommand, BaseOptions } from 'src/commands/base.command';
5
6
  import { ConnectionService } from 'src/services/connection';
6
7
  import { LoggerService } from 'src/services/common';
@@ -28,13 +29,10 @@ export class SaveSchemaCommand extends BaseCommand {
28
29
  }
29
30
 
30
31
  await this.connectionService.connect(options);
31
- await this.saveAllTableSchemas(
32
- this.connectionService.revisionId,
33
- options.folder,
34
- );
32
+ await this.saveAllTableSchemas(options.folder);
35
33
  }
36
34
 
37
- private async saveAllTableSchemas(revisionId: string, folderPath: string) {
35
+ private async saveAllTableSchemas(folderPath: string) {
38
36
  try {
39
37
  await mkdir(folderPath, { recursive: true });
40
38
 
@@ -43,19 +41,16 @@ export class SaveSchemaCommand extends BaseCommand {
43
41
  let totalTables = 0;
44
42
 
45
43
  const { processed } = await fetchAndProcessPages(
46
- (params) => this.api.tables({ revisionId, ...params }),
44
+ (params) =>
45
+ this.revisionScope.getTables(params).then((data) => ({ data })),
47
46
  async (table, index) => {
48
47
  this.logger.processingTable(table.id);
49
48
 
50
- const schemaResult = await this.api.tableSchema(revisionId, table.id);
49
+ const schema = await this.revisionScope.getTableSchema(table.id);
51
50
  const fileName = `${table.id}.json`;
52
51
  const filePath = join(folderPath, fileName);
53
52
 
54
- await writeFile(
55
- filePath,
56
- JSON.stringify(schemaResult.data, null, 2),
57
- 'utf-8',
58
- );
53
+ await writeFile(filePath, JSON.stringify(schema, null, 2), 'utf-8');
59
54
 
60
55
  this.logger.success(
61
56
  `Saved schema: ${fileName} (${index + 1}/${totalTables})`,
@@ -80,8 +75,8 @@ export class SaveSchemaCommand extends BaseCommand {
80
75
  }
81
76
  }
82
77
 
83
- private get api() {
84
- return this.connectionService.api;
78
+ private get revisionScope(): RevisionScope {
79
+ return this.connectionService.revisionScope;
85
80
  }
86
81
 
87
82
  @Option({
@@ -3,6 +3,45 @@ import { sharedFields } from 'src/config/shared-fields';
3
3
 
4
4
  // https://json-schema.org/specification#single-vocabulary-meta-schemas
5
5
 
6
+ export const xFormulaSchema: Schema = {
7
+ type: 'object',
8
+ properties: {
9
+ version: {
10
+ const: 1,
11
+ },
12
+ expression: {
13
+ type: 'string',
14
+ minLength: 1,
15
+ maxLength: 10000,
16
+ },
17
+ },
18
+ additionalProperties: false,
19
+ required: ['version', 'expression'],
20
+ };
21
+
22
+ // When x-formula is present, readOnly must be true
23
+ export const xFormulaRequiresReadOnly: Schema = {
24
+ if: {
25
+ properties: { 'x-formula': { type: 'object' } },
26
+ required: ['x-formula'],
27
+ },
28
+ then: {
29
+ properties: { readOnly: { const: true } },
30
+ required: ['readOnly'],
31
+ },
32
+ };
33
+
34
+ // foreignKey and x-formula are mutually exclusive
35
+ export const foreignKeyExcludesFormula: Schema = {
36
+ if: {
37
+ properties: { foreignKey: { type: 'string' } },
38
+ required: ['foreignKey'],
39
+ },
40
+ then: {
41
+ not: { required: ['x-formula'] },
42
+ },
43
+ };
44
+
6
45
  export const refMetaSchema: Schema = {
7
46
  type: 'object',
8
47
  properties: {
@@ -60,18 +99,22 @@ export const stringMetaSchema: Schema = {
60
99
  foreignKey: {
61
100
  type: 'string',
62
101
  },
102
+ 'x-formula': xFormulaSchema,
63
103
  },
64
104
  additionalProperties: false,
65
105
  required: ['type', 'default'],
106
+ allOf: [xFormulaRequiresReadOnly, foreignKeyExcludesFormula],
66
107
  };
67
108
 
68
109
  export const noForeignKeyStringMetaSchema: Schema = {
69
110
  type: 'object',
70
111
  properties: {
71
112
  ...baseStringFields,
113
+ 'x-formula': xFormulaSchema,
72
114
  },
73
115
  additionalProperties: false,
74
116
  required: ['type', 'default'],
117
+ ...xFormulaRequiresReadOnly,
75
118
  };
76
119
 
77
120
  export const numberMetaSchema: Schema = {
@@ -87,9 +130,11 @@ export const numberMetaSchema: Schema = {
87
130
  type: 'boolean',
88
131
  },
89
132
  ...sharedFields,
133
+ 'x-formula': xFormulaSchema,
90
134
  },
91
135
  additionalProperties: false,
92
136
  required: ['type', 'default'],
137
+ ...xFormulaRequiresReadOnly,
93
138
  };
94
139
 
95
140
  export const booleanMetaSchema: Schema = {
@@ -105,9 +150,11 @@ export const booleanMetaSchema: Schema = {
105
150
  type: 'boolean',
106
151
  },
107
152
  ...sharedFields,
153
+ 'x-formula': xFormulaSchema,
108
154
  },
109
155
  additionalProperties: false,
110
156
  required: ['type', 'default'],
157
+ ...xFormulaRequiresReadOnly,
111
158
  };
112
159
 
113
160
  export const objectMetaSchema: Schema = {
@@ -0,0 +1,117 @@
1
+ import { ConnectionFactoryService } from '../connection-factory.service';
2
+ import { UrlBuilderService, RevisiumUrlComplete } from '../../url';
3
+ import { LoggerService } from '../../common';
4
+ import { RevisiumApiClient } from '../api-client';
5
+
6
+ jest.mock('../api-client');
7
+
8
+ describe('ConnectionFactoryService', () => {
9
+ let service: ConnectionFactoryService;
10
+ let urlBuilderFake: { formatAsRevisiumUrl: jest.Mock };
11
+ let loggerFake: {
12
+ connecting: jest.Mock;
13
+ connected: jest.Mock;
14
+ authenticated: jest.Mock;
15
+ };
16
+
17
+ const mockDraftScope = {
18
+ revisionId: 'draft-456',
19
+ isDraft: true,
20
+ };
21
+
22
+ const mockHeadScope = {
23
+ revisionId: 'head-123',
24
+ isDraft: false,
25
+ };
26
+
27
+ const mockExplicitScope = {
28
+ revisionId: 'specific-rev-id',
29
+ isDraft: false,
30
+ };
31
+
32
+ const mockBranchScope = {
33
+ draft: jest.fn().mockReturnValue(mockDraftScope),
34
+ head: jest.fn().mockReturnValue(mockHeadScope),
35
+ revision: jest.fn().mockResolvedValue(mockExplicitScope),
36
+ headRevisionId: 'head-123',
37
+ draftRevisionId: 'draft-456',
38
+ };
39
+
40
+ const baseUrl: RevisiumUrlComplete = {
41
+ baseUrl: 'https://cloud.revisium.io',
42
+ organization: 'test-org',
43
+ project: 'test-project',
44
+ branch: 'main',
45
+ revision: 'draft',
46
+ auth: { method: 'token', token: 'test-token' },
47
+ };
48
+
49
+ beforeEach(() => {
50
+ urlBuilderFake = {
51
+ formatAsRevisiumUrl: jest.fn().mockReturnValue('revisium://...'),
52
+ };
53
+
54
+ loggerFake = {
55
+ connecting: jest.fn(),
56
+ connected: jest.fn(),
57
+ authenticated: jest.fn(),
58
+ };
59
+
60
+ const MockApiClient = RevisiumApiClient as jest.MockedClass<
61
+ typeof RevisiumApiClient
62
+ >;
63
+ MockApiClient.mockImplementation(
64
+ () =>
65
+ ({
66
+ client: {
67
+ branch: jest.fn().mockResolvedValue(mockBranchScope),
68
+ },
69
+ authenticate: jest.fn().mockResolvedValue('test-user'),
70
+ }) as unknown as RevisiumApiClient,
71
+ );
72
+
73
+ service = new ConnectionFactoryService(
74
+ urlBuilderFake as unknown as UrlBuilderService,
75
+ loggerFake as unknown as LoggerService,
76
+ );
77
+ });
78
+
79
+ afterEach(() => {
80
+ jest.clearAllMocks();
81
+ });
82
+
83
+ describe('revision resolution', () => {
84
+ it('uses draft scope when revision is draft', async () => {
85
+ const url = { ...baseUrl, revision: 'draft' };
86
+
87
+ const result = await service.createConnection(url);
88
+
89
+ expect(mockBranchScope.draft).toHaveBeenCalled();
90
+ expect(mockBranchScope.head).not.toHaveBeenCalled();
91
+ expect(mockBranchScope.revision).not.toHaveBeenCalled();
92
+ expect(result.revisionScope).toBe(mockDraftScope);
93
+ });
94
+
95
+ it('uses head scope when revision is head', async () => {
96
+ const url = { ...baseUrl, revision: 'head' };
97
+
98
+ const result = await service.createConnection(url);
99
+
100
+ expect(mockBranchScope.head).toHaveBeenCalled();
101
+ expect(mockBranchScope.draft).not.toHaveBeenCalled();
102
+ expect(mockBranchScope.revision).not.toHaveBeenCalled();
103
+ expect(result.revisionScope).toBe(mockHeadScope);
104
+ });
105
+
106
+ it('uses explicit revision scope for specific revision ID', async () => {
107
+ const url = { ...baseUrl, revision: 'specific-rev-id' };
108
+
109
+ const result = await service.createConnection(url);
110
+
111
+ expect(mockBranchScope.revision).toHaveBeenCalledWith('specific-rev-id');
112
+ expect(mockBranchScope.draft).not.toHaveBeenCalled();
113
+ expect(mockBranchScope.head).not.toHaveBeenCalled();
114
+ expect(result.revisionScope).toBe(mockExplicitScope);
115
+ });
116
+ });
117
+ });