@ldsg/cli 0.3.29 → 0.3.30

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 (77) hide show
  1. package/.rush/temp/package-deps_build.json +22 -5
  2. package/.tsbuildinfo +1 -1
  3. package/CHANGELOG.json +26 -0
  4. package/CHANGELOG.md +8 -1
  5. package/dist/__tests__/commands.test.d.ts +1 -0
  6. package/dist/__tests__/commands.test.js +219 -0
  7. package/dist/__tests__/commands.test.js.map +1 -0
  8. package/dist/__tests__/formatter.test.d.ts +1 -0
  9. package/dist/__tests__/formatter.test.js +114 -0
  10. package/dist/__tests__/formatter.test.js.map +1 -0
  11. package/dist/__tests__/json-file-mutator.test.d.ts +1 -0
  12. package/dist/__tests__/json-file-mutator.test.js +103 -0
  13. package/dist/__tests__/json-file-mutator.test.js.map +1 -0
  14. package/dist/__tests__/source-resolver.test.d.ts +1 -0
  15. package/dist/__tests__/source-resolver.test.js +101 -0
  16. package/dist/__tests__/source-resolver.test.js.map +1 -0
  17. package/dist/cli.js +304 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/resource/convert.d.ts +11 -0
  20. package/dist/commands/resource/convert.js +42 -0
  21. package/dist/commands/resource/convert.js.map +1 -0
  22. package/dist/commands/resource/create.d.ts +14 -0
  23. package/dist/commands/resource/create.js +37 -0
  24. package/dist/commands/resource/create.js.map +1 -0
  25. package/dist/commands/resource/delete.d.ts +10 -0
  26. package/dist/commands/resource/delete.js +10 -0
  27. package/dist/commands/resource/delete.js.map +1 -0
  28. package/dist/commands/resource/diff.d.ts +10 -0
  29. package/dist/commands/resource/diff.js +72 -0
  30. package/dist/commands/resource/diff.js.map +1 -0
  31. package/dist/commands/resource/get.d.ts +6 -0
  32. package/dist/commands/resource/get.js +6 -0
  33. package/dist/commands/resource/get.js.map +1 -0
  34. package/dist/commands/resource/list.d.ts +30 -0
  35. package/dist/commands/resource/list.js +138 -0
  36. package/dist/commands/resource/list.js.map +1 -0
  37. package/dist/commands/resource/tree.d.ts +2 -0
  38. package/dist/commands/resource/tree.js +6 -0
  39. package/dist/commands/resource/tree.js.map +1 -0
  40. package/dist/commands/resource/update.d.ts +11 -0
  41. package/dist/commands/resource/update.js +13 -0
  42. package/dist/commands/resource/update.js.map +1 -0
  43. package/dist/constants.d.ts +2 -0
  44. package/dist/constants.js +3 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/types/resource-record.d.ts +43 -0
  47. package/dist/types/resource-record.js +3 -0
  48. package/dist/types/resource-record.js.map +1 -0
  49. package/dist/utils/formatter.d.ts +12 -0
  50. package/dist/utils/formatter.js +96 -0
  51. package/dist/utils/formatter.js.map +1 -0
  52. package/dist/utils/json-file-mutator.d.ts +15 -0
  53. package/dist/utils/json-file-mutator.js +109 -0
  54. package/dist/utils/json-file-mutator.js.map +1 -0
  55. package/dist/utils/source-resolver.d.ts +4 -0
  56. package/dist/utils/source-resolver.js +93 -0
  57. package/dist/utils/source-resolver.js.map +1 -0
  58. package/jest.config.js +12 -0
  59. package/package.json +8 -6
  60. package/src/__tests__/commands.test.ts +262 -0
  61. package/src/__tests__/formatter.test.ts +133 -0
  62. package/src/__tests__/json-file-mutator.test.ts +136 -0
  63. package/src/__tests__/source-resolver.test.ts +125 -0
  64. package/src/cli.ts +309 -1
  65. package/src/commands/resource/convert.ts +52 -0
  66. package/src/commands/resource/create.ts +49 -0
  67. package/src/commands/resource/delete.ts +15 -0
  68. package/src/commands/resource/diff.ts +104 -0
  69. package/src/commands/resource/get.ts +5 -0
  70. package/src/commands/resource/list.ts +161 -0
  71. package/src/commands/resource/tree.ts +4 -0
  72. package/src/commands/resource/update.ts +21 -0
  73. package/src/constants.ts +3 -0
  74. package/src/types/resource-record.ts +52 -0
  75. package/src/utils/formatter.ts +106 -0
  76. package/src/utils/json-file-mutator.ts +137 -0
  77. package/src/utils/source-resolver.ts +102 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-resolver.js","sourceRoot":"","sources":["../../src/utils/source-resolver.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,12 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ testMatch: ['**/__tests__/**/*.test.ts'],
6
+ moduleNameMapper: {
7
+ '^@ldsg/types$': '<rootDir>/../types/src',
8
+ '^@ldsg/constants$': '<rootDir>/../constants/src',
9
+ '^@ldsg/apaas-to-runtime$': '<rootDir>/../apaas-to-runtime/src',
10
+ '^@ldsg/utils$': '<rootDir>/../utils/src'
11
+ }
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldsg/cli",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "license": "ISC",
@@ -14,9 +14,9 @@
14
14
  "commander": "^12.0.0",
15
15
  "fs-extra": "^11.3.0",
16
16
  "jsonata": "^2.0.6",
17
- "@ldsg/apaas-to-runtime": "0.3.29",
18
- "@ldsg/constants": "0.3.29",
19
- "@ldsg/utils": "0.3.29"
17
+ "@ldsg/apaas-to-runtime": "0.3.30",
18
+ "@ldsg/constants": "0.3.30",
19
+ "@ldsg/utils": "0.3.30"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/fs-extra": "^11.0.4",
@@ -32,13 +32,15 @@
32
32
  "ts-node": "^10.9.2",
33
33
  "tslib": "^2.8.1",
34
34
  "typescript": "^5.3.3",
35
- "@ldsg/types": "0.3.29"
35
+ "@ldsg/types": "0.3.30"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc --project tsconfig.build.json",
42
- "cli": "ts-node src/cli.ts"
42
+ "cli": "ts-node src/cli.ts",
43
+ "test": "jest",
44
+ "test:watch": "jest --watch"
43
45
  }
44
46
  }
@@ -0,0 +1,262 @@
1
+ import { ResourceRecords } from '@ldsg/types';
2
+ import { listResources, getResource, getTree } from '../commands/resource/list';
3
+ import { createResource } from '../commands/resource/create';
4
+ import { updateResource } from '../commands/resource/update';
5
+ import { deleteResource } from '../commands/resource/delete';
6
+ import { diffResource } from '../commands/resource/diff';
7
+ import { SourceData } from '../types/resource-record';
8
+ import fs from 'fs-extra';
9
+
10
+ jest.mock('fs-extra');
11
+
12
+ const mockedFs = fs as jest.Mocked<typeof fs>;
13
+
14
+ const sampleRecords: ResourceRecords = [
15
+ { id: 'app1', kind: 'application', parentId: null, settings: { title: 'My App' } },
16
+ { id: 'form1', kind: 'form', parentId: 'app1', settings: { title: 'Login Form' } },
17
+ { id: 'form2', kind: 'form', parentId: 'app1', settings: { title: 'Register Form' } },
18
+ { id: 'table1', kind: 'table', parentId: 'app1', settings: { title: 'Users' } },
19
+ { id: 'field1', kind: 'field', parentId: 'table1', settings: { title: 'Name' } },
20
+ { id: 'field2', kind: 'field', parentId: 'table1', settings: { title: 'Email' } }
21
+ ];
22
+
23
+ const sourceData: SourceData = {
24
+ resourceRecords: sampleRecords,
25
+ format: 'array',
26
+ filePath: '/test/data.json'
27
+ };
28
+
29
+ describe('commands', () => {
30
+ beforeEach(() => {
31
+ jest.resetAllMocks();
32
+ mockedFs.readJson.mockResolvedValue([...sampleRecords]);
33
+ });
34
+
35
+ describe('listResources', () => {
36
+ it('should return all records without filter', async () => {
37
+ const result = await listResources(sampleRecords, {});
38
+ expect(result.total).toBe(6);
39
+ expect(result.resources).toHaveLength(6);
40
+ });
41
+
42
+ it('should filter by kind', async () => {
43
+ const result = await listResources(sampleRecords, { kind: 'form' });
44
+ expect(result.total).toBe(2);
45
+ expect(result.resources.every((r) => r.kind === 'form')).toBe(true);
46
+ });
47
+
48
+ it('should filter by parentId', async () => {
49
+ const result = await listResources(sampleRecords, { parentId: 'table1' });
50
+ expect(result.total).toBe(2);
51
+ expect(result.resources.every((r) => r.parentId === 'table1')).toBe(true);
52
+ });
53
+
54
+ it('should filter by rootId', async () => {
55
+ const result = await listResources(sampleRecords, { rootId: 'table1' });
56
+ expect(result.total).toBe(3);
57
+ expect(result.resources.map((r) => r.id)).toEqual(['table1', 'field1', 'field2']);
58
+ });
59
+
60
+ it('should combine filters', async () => {
61
+ const result = await listResources(sampleRecords, { kind: 'field', rootId: 'table1' });
62
+ expect(result.total).toBe(2);
63
+ });
64
+ });
65
+
66
+ describe('getResource', () => {
67
+ it('should get a single resource by id', async () => {
68
+ const result = await getResource(sampleRecords, 'app1', {});
69
+ expect(result).not.toBeNull();
70
+ expect(result!.id).toBe('app1');
71
+ });
72
+
73
+ it('should return null for non-existent id', async () => {
74
+ const result = await getResource(sampleRecords, 'nonexistent', {});
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it('should include children when requested', async () => {
79
+ const result = (await getResource(sampleRecords, 'app1', { children: true })) as any;
80
+ expect(result.children).toHaveLength(3);
81
+ expect(result.children.map((c: any) => c.id)).toEqual(['form1', 'form2', 'table1']);
82
+ });
83
+
84
+ it('should return null for non-existent id with tree option', async () => {
85
+ const result = await getResource(sampleRecords, 'nonexistent', { tree: true });
86
+ expect(result).toBeNull();
87
+ });
88
+ });
89
+
90
+ describe('getTree', () => {
91
+ it('should build tree for specified root', async () => {
92
+ const result = await getTree(sampleRecords, 'table1');
93
+ expect(result.rootId).toBe('table1');
94
+ expect(result.tree.resource.id).toBe('table1');
95
+ expect(result.tree.children).toHaveLength(2);
96
+ });
97
+
98
+ it('should auto-detect root when only one', async () => {
99
+ const singleRoot: ResourceRecords = [
100
+ { id: 'app1', kind: 'application', parentId: null, settings: {} },
101
+ { id: 'form1', kind: 'form', parentId: 'app1', settings: {} }
102
+ ];
103
+ const result = await getTree(singleRoot, undefined);
104
+ expect(result.rootId).toBe('app1');
105
+ });
106
+
107
+ it('should throw for non-existent root', async () => {
108
+ await expect(getTree(sampleRecords, 'nonexistent')).rejects.toThrow('not found');
109
+ });
110
+ });
111
+
112
+ describe('createResource', () => {
113
+ it('should create with explicit fields', async () => {
114
+ mockedFs.readJson.mockResolvedValue([...sampleRecords]);
115
+
116
+ const result = await createResource({
117
+ sourceData,
118
+ kind: 'form',
119
+ parentId: 'app1',
120
+ settings: { title: 'New Form' }
121
+ });
122
+
123
+ expect(result.record.kind).toBe('form');
124
+ expect(result.record.parentId).toBe('app1');
125
+ expect(result.record.settings).toEqual({ title: 'New Form' });
126
+ expect(mockedFs.writeJson).toHaveBeenCalled();
127
+ });
128
+
129
+ it('should throw without kind', async () => {
130
+ await expect(
131
+ createResource({
132
+ sourceData,
133
+ parentId: 'app1'
134
+ } as any)
135
+ ).rejects.toThrow('--kind');
136
+ });
137
+
138
+ it('should throw without parent-id', async () => {
139
+ await expect(
140
+ createResource({
141
+ sourceData,
142
+ kind: 'form'
143
+ } as any)
144
+ ).rejects.toThrow('--parent-id');
145
+ });
146
+ });
147
+
148
+ describe('updateResource', () => {
149
+ it('should update existing resource', async () => {
150
+ mockedFs.readJson.mockResolvedValue([...sampleRecords]);
151
+
152
+ const result = await updateResource({
153
+ sourceData,
154
+ id: 'form1',
155
+ settings: { title: 'Updated Form' }
156
+ });
157
+
158
+ expect(result.record.id).toBe('form1');
159
+ expect(result.record.settings).toEqual({ title: 'Updated Form' });
160
+ expect(mockedFs.writeJson).toHaveBeenCalled();
161
+ });
162
+
163
+ it('should throw on non-existent id', async () => {
164
+ await expect(
165
+ updateResource({
166
+ sourceData,
167
+ id: 'nonexistent',
168
+ settings: { title: 'x' }
169
+ })
170
+ ).rejects.toThrow('not found');
171
+ });
172
+ });
173
+
174
+ describe('deleteResource', () => {
175
+ it('should delete resource and children', async () => {
176
+ mockedFs.readJson.mockResolvedValue([...sampleRecords]);
177
+
178
+ const result = await deleteResource({
179
+ sourceData,
180
+ id: 'table1'
181
+ });
182
+
183
+ expect(result.removed).toHaveLength(3);
184
+ expect(result.removed.map((r) => r.id)).toEqual(['table1', 'field1', 'field2']);
185
+ });
186
+
187
+ it('should throw on non-existent id', async () => {
188
+ await expect(
189
+ deleteResource({
190
+ sourceData,
191
+ id: 'nonexistent'
192
+ })
193
+ ).rejects.toThrow('not found');
194
+ });
195
+ });
196
+
197
+ describe('diffResource', () => {
198
+ const recordsA: ResourceRecords = [
199
+ { id: '1', kind: 'application', parentId: null, settings: { title: 'App A', version: 1 } }
200
+ ];
201
+ const recordsB: ResourceRecords = [
202
+ {
203
+ id: '1',
204
+ kind: 'application',
205
+ parentId: null,
206
+ settings: { title: 'App B', version: 1, newField: 'new' }
207
+ }
208
+ ];
209
+
210
+ it('should detect changed fields', async () => {
211
+ const result = await diffResource({
212
+ sourceA: 'a.json',
213
+ sourceB: 'b.json',
214
+ id: '1',
215
+ recordsA,
216
+ recordsB
217
+ });
218
+
219
+ expect(result.changed.length).toBeGreaterThan(0);
220
+ expect(result.changed.some((c) => c.path === 'title')).toBe(true);
221
+ });
222
+
223
+ it('should detect added fields', async () => {
224
+ const result = await diffResource({
225
+ sourceA: 'a.json',
226
+ sourceB: 'b.json',
227
+ id: '1',
228
+ recordsA,
229
+ recordsB
230
+ });
231
+
232
+ expect(result.added.some((a) => a.path === 'newField')).toBe(true);
233
+ });
234
+
235
+ it('should handle resource only in one side', async () => {
236
+ const result = await diffResource({
237
+ sourceA: 'a.json',
238
+ sourceB: 'b.json',
239
+ id: '2',
240
+ recordsA: [],
241
+ recordsB: [{ id: '2', kind: 'application', parentId: null, settings: {} }]
242
+ });
243
+
244
+ expect(result.sideA).toBeNull();
245
+ expect(result.sideB).not.toBeNull();
246
+ });
247
+
248
+ it('should report no differences for identical resources', async () => {
249
+ const result = await diffResource({
250
+ sourceA: 'a.json',
251
+ sourceB: 'b.json',
252
+ id: '1',
253
+ recordsA,
254
+ recordsB: recordsA
255
+ });
256
+
257
+ expect(result.changed).toHaveLength(0);
258
+ expect(result.added).toHaveLength(0);
259
+ expect(result.removed).toHaveLength(0);
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,133 @@
1
+ import {
2
+ formatSuccess,
3
+ formatError,
4
+ formatJson,
5
+ formatPrettyList,
6
+ formatPrettyTree,
7
+ formatPrettyDiff
8
+ } from '../utils/formatter';
9
+
10
+ describe('formatter', () => {
11
+ describe('formatSuccess', () => {
12
+ it('should format success response', () => {
13
+ const result = formatSuccess({ id: '1' });
14
+ expect(result).toEqual({ success: true, data: { id: '1' } });
15
+ });
16
+ });
17
+
18
+ describe('formatError', () => {
19
+ it('should format error response', () => {
20
+ const result = formatError('NOT_FOUND', 'Resource not found');
21
+ expect(result).toEqual({ success: false, error: { code: 'NOT_FOUND', message: 'Resource not found' } });
22
+ });
23
+ });
24
+
25
+ describe('formatJson', () => {
26
+ it('should format JSON with 2-space indent', () => {
27
+ const result = formatJson({ a: 1, b: 'test' });
28
+ expect(result).toBe('{\n "a": 1,\n "b": "test"\n}');
29
+ });
30
+ });
31
+
32
+ describe('formatPrettyList', () => {
33
+ it('should return message for empty list', () => {
34
+ expect(formatPrettyList([])).toBe('No resources found.');
35
+ });
36
+
37
+ it('should format list with header and rows', () => {
38
+ const resources = [
39
+ { id: '1', kind: 'application', parentId: null, title: 'App1' },
40
+ { id: '2', kind: 'form', parentId: '1', title: 'Form1' }
41
+ ];
42
+ const result = formatPrettyList(resources);
43
+ expect(result).toContain('ID');
44
+ expect(result).toContain('KIND');
45
+ expect(result).toContain('PARENT ID');
46
+ expect(result).toContain('Total: 2 resource(s)');
47
+ });
48
+ });
49
+
50
+ describe('formatPrettyTree', () => {
51
+ it('should format a tree with root and children', () => {
52
+ const tree = {
53
+ resource: { id: '1', kind: 'application', parentId: null, settings: {} },
54
+ children: [
55
+ {
56
+ resource: { id: '2', kind: 'form', parentId: '1', settings: {} },
57
+ children: []
58
+ }
59
+ ]
60
+ };
61
+
62
+ const result = formatPrettyTree(tree, false);
63
+ expect(result).toContain('1');
64
+ expect(result).toContain('2');
65
+ expect(result).toContain('└──');
66
+ });
67
+
68
+ it('should show kind prefix with showKind option', () => {
69
+ const tree = {
70
+ resource: { id: '1', kind: 'application', parentId: null, settings: {} },
71
+ children: []
72
+ };
73
+
74
+ const result = formatPrettyTree(tree, true);
75
+ expect(result).toContain('[application]');
76
+ });
77
+ });
78
+
79
+ describe('formatPrettyDiff', () => {
80
+ it('should show no differences message', () => {
81
+ const diff = {
82
+ id: '1',
83
+ kind: 'application',
84
+ sourceA: 'a.json',
85
+ sourceB: 'b.json',
86
+ changed: [],
87
+ added: [],
88
+ removed: [],
89
+ sideA: { id: '1', kind: 'application', parentId: null, settings: {} },
90
+ sideB: { id: '1', kind: 'application', parentId: null, settings: {} }
91
+ } as any;
92
+
93
+ const result = formatPrettyDiff(diff);
94
+ expect(result).toContain('No differences found.');
95
+ });
96
+
97
+ it('should show changed fields', () => {
98
+ const diff = {
99
+ id: '1',
100
+ kind: 'application',
101
+ sourceA: 'a.json',
102
+ sourceB: 'b.json',
103
+ changed: [{ path: 'settings.title', from: 'Old', to: 'New' }],
104
+ added: [],
105
+ removed: [],
106
+ sideA: { id: '1', kind: 'application', parentId: null, settings: {} },
107
+ sideB: { id: '1', kind: 'application', parentId: null, settings: {} }
108
+ } as any;
109
+
110
+ const result = formatPrettyDiff(diff);
111
+ expect(result).toContain('settings.title');
112
+ expect(result).toContain('Old');
113
+ expect(result).toContain('New');
114
+ });
115
+
116
+ it('should show side-only existence', () => {
117
+ const diffA: any = {
118
+ id: '1',
119
+ kind: 'application',
120
+ sourceA: 'a.json',
121
+ sourceB: 'b.json',
122
+ changed: [],
123
+ added: [],
124
+ removed: [],
125
+ sideA: null,
126
+ sideB: { id: '1', kind: 'application', parentId: null, settings: {} }
127
+ };
128
+
129
+ const result = formatPrettyDiff(diffA);
130
+ expect(result).toContain('only exists in source B');
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,136 @@
1
+ import fs from 'fs-extra';
2
+ import { appendRecord, updateRecord, removeRecord, generateId } from '../utils/json-file-mutator';
3
+ import { SourceData } from '../types/resource-record';
4
+ import { ResourceRecords } from '@ldsg/types';
5
+
6
+ jest.mock('fs-extra');
7
+
8
+ const mockedFs = fs as jest.Mocked<typeof fs>;
9
+
10
+ describe('json-file-mutator', () => {
11
+ const arrayRecords: ResourceRecords = [
12
+ { id: '1', kind: 'application', parentId: null, settings: { title: 'App1' } },
13
+ { id: '2', kind: 'form', parentId: '1', settings: { title: 'Form1' } },
14
+ { id: '3', kind: 'table', parentId: '1', settings: { title: 'Table1' } },
15
+ { id: '4', kind: 'field', parentId: '3', settings: { title: 'Field1' } }
16
+ ];
17
+
18
+ const arraySourceData: SourceData = {
19
+ resourceRecords: arrayRecords,
20
+ format: 'array',
21
+ filePath: '/test/data.json'
22
+ };
23
+
24
+ const objectSourceData: SourceData = {
25
+ resourceRecords: arrayRecords,
26
+ format: 'object',
27
+ filePath: '/test/data.json'
28
+ };
29
+
30
+ beforeEach(() => {
31
+ jest.resetAllMocks();
32
+ mockedFs.readJson.mockResolvedValue([...arrayRecords]);
33
+ });
34
+
35
+ describe('generateId', () => {
36
+ it('should generate a string id', () => {
37
+ const id = generateId();
38
+ expect(typeof id).toBe('string');
39
+ expect(id.length).toBeGreaterThan(5);
40
+ });
41
+
42
+ it('should generate unique ids', () => {
43
+ const ids = new Set(Array.from({ length: 10 }, () => generateId()));
44
+ expect(ids.size).toBe(10);
45
+ });
46
+ });
47
+
48
+ describe('appendRecord', () => {
49
+ it('should append a new record to array format', async () => {
50
+ mockedFs.readJson.mockResolvedValue([...arrayRecords]);
51
+
52
+ const newRecord = { id: '5', kind: 'form', parentId: '1', settings: { title: 'Form2' } };
53
+ const result = await appendRecord(arraySourceData, newRecord);
54
+
55
+ expect(result.record.id).toBe('5');
56
+ expect(result.allRecords).toHaveLength(5);
57
+ expect(mockedFs.writeJson).toHaveBeenCalledWith(
58
+ '/test/data.json',
59
+ expect.arrayContaining([expect.objectContaining({ id: '5' })]),
60
+ { spaces: 2 }
61
+ );
62
+ });
63
+
64
+ it('should append to object format preserving structure', async () => {
65
+ mockedFs.readJson.mockResolvedValue({ resourceRecords: [...arrayRecords] });
66
+
67
+ const newRecord = { id: '5', kind: 'form', parentId: '1', settings: {} };
68
+ await appendRecord(objectSourceData, newRecord);
69
+
70
+ expect(mockedFs.writeJson).toHaveBeenCalledWith(
71
+ '/test/data.json',
72
+ expect.objectContaining({
73
+ resourceRecords: expect.arrayContaining([expect.objectContaining({ id: '5' })])
74
+ }),
75
+ { spaces: 2 }
76
+ );
77
+ });
78
+
79
+ it('should throw on duplicate id', async () => {
80
+ const dupRecord = { id: '1', kind: 'application', parentId: null, settings: {} };
81
+ await expect(appendRecord(arraySourceData, dupRecord)).rejects.toThrow('already exists');
82
+ });
83
+
84
+ it('should not write file in dry-run mode', async () => {
85
+ const newRecord = { id: '5', kind: 'form', parentId: '1', settings: {} };
86
+ await appendRecord(arraySourceData, newRecord, true);
87
+
88
+ expect(mockedFs.writeJson).not.toHaveBeenCalled();
89
+ });
90
+ });
91
+
92
+ describe('updateRecord', () => {
93
+ it('should update settings and merge', async () => {
94
+ mockedFs.readJson.mockResolvedValue([...arrayRecords]);
95
+
96
+ const result = await updateRecord(arraySourceData, '1', { title: 'Updated App', newField: 'value' });
97
+
98
+ expect(result.record.settings).toEqual({ title: 'Updated App', newField: 'value' });
99
+ expect(mockedFs.writeJson).toHaveBeenCalled();
100
+ });
101
+
102
+ it('should throw on non-existent id', async () => {
103
+ await expect(updateRecord(arraySourceData, 'nonexistent', { title: 'x' })).rejects.toThrow('not found');
104
+ });
105
+
106
+ it('should not write file in dry-run mode', async () => {
107
+ await updateRecord(arraySourceData, '1', { title: 'Dry Run' }, true);
108
+
109
+ expect(mockedFs.writeJson).not.toHaveBeenCalled();
110
+ });
111
+ });
112
+
113
+ describe('removeRecord', () => {
114
+ it('should remove record and its children', async () => {
115
+ mockedFs.readJson.mockResolvedValue([...arrayRecords]);
116
+
117
+ const result = await removeRecord(arraySourceData, '3');
118
+
119
+ expect(result.removed).toHaveLength(2);
120
+ expect(result.removed.map((r) => r.id)).toEqual(['3', '4']);
121
+ expect(result.allRecords).toHaveLength(2);
122
+ expect(result.allRecords.map((r) => r.id)).toEqual(['1', '2']);
123
+ });
124
+
125
+ it('should throw on non-existent id', async () => {
126
+ await expect(removeRecord(arraySourceData, 'nonexistent')).rejects.toThrow('not found');
127
+ });
128
+
129
+ it('should not write file in dry-run mode', async () => {
130
+ const result = await removeRecord(arraySourceData, '3', true);
131
+
132
+ expect(result.removed).toHaveLength(2);
133
+ expect(mockedFs.writeJson).not.toHaveBeenCalled();
134
+ });
135
+ });
136
+ });