@friggframework/admin-scripts 2.0.0--canary.517.7e78259.0 → 2.0.0--canary.517.2239974.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.
@@ -46,104 +46,13 @@ describe('AdminScriptBase', () => {
46
46
  });
47
47
 
48
48
  it('should have clean display object without redundant fields', () => {
49
- // Default display should only have UI-specific fields
50
49
  expect(AdminScriptBase.Definition.display).toBeDefined();
51
50
  expect(AdminScriptBase.Definition.display.category).toBe('maintenance');
52
- // Should NOT have redundant label/description
53
51
  expect(AdminScriptBase.Definition.display.label).toBeUndefined();
54
52
  expect(AdminScriptBase.Definition.display.description).toBeUndefined();
55
53
  });
56
54
  });
57
55
 
58
- describe('Static methods', () => {
59
- it('getName() should return the script name', () => {
60
- class TestScript extends AdminScriptBase {
61
- static Definition = {
62
- name: 'my-script',
63
- version: '1.0.0',
64
- description: 'test',
65
- };
66
- }
67
-
68
- expect(TestScript.getName()).toBe('my-script');
69
- });
70
-
71
- it('getCurrentVersion() should return the version', () => {
72
- class TestScript extends AdminScriptBase {
73
- static Definition = {
74
- name: 'my-script',
75
- version: '2.3.1',
76
- description: 'test',
77
- };
78
- }
79
-
80
- expect(TestScript.getCurrentVersion()).toBe('2.3.1');
81
- });
82
-
83
- it('getDefinition() should return the full Definition', () => {
84
- class TestScript extends AdminScriptBase {
85
- static Definition = {
86
- name: 'my-script',
87
- version: '1.0.0',
88
- description: 'test',
89
- source: 'USER_DEFINED',
90
- };
91
- }
92
-
93
- const definition = TestScript.getDefinition();
94
- expect(definition).toEqual({
95
- name: 'my-script',
96
- version: '1.0.0',
97
- description: 'test',
98
- source: 'USER_DEFINED',
99
- });
100
- });
101
-
102
- it('getDisplayLabel() should return display.label or fall back to name', () => {
103
- class ScriptWithLabel extends AdminScriptBase {
104
- static Definition = {
105
- name: 'my-script',
106
- version: '1.0.0',
107
- description: 'test',
108
- display: { label: 'My Custom Label' },
109
- };
110
- }
111
-
112
- class ScriptWithoutLabel extends AdminScriptBase {
113
- static Definition = {
114
- name: 'another-script',
115
- version: '1.0.0',
116
- description: 'test',
117
- };
118
- }
119
-
120
- expect(ScriptWithLabel.getDisplayLabel()).toBe('My Custom Label');
121
- expect(ScriptWithoutLabel.getDisplayLabel()).toBe('another-script');
122
- });
123
-
124
- it('getDisplayDescription() should return display.description or fall back to description', () => {
125
- class ScriptWithDisplayDesc extends AdminScriptBase {
126
- static Definition = {
127
- name: 'my-script',
128
- version: '1.0.0',
129
- description: 'Technical description',
130
- display: { description: 'User-friendly description' },
131
- };
132
- }
133
-
134
- class ScriptWithoutDisplayDesc extends AdminScriptBase {
135
- static Definition = {
136
- name: 'another-script',
137
- version: '1.0.0',
138
- description: 'Technical description',
139
- };
140
- }
141
-
142
- expect(ScriptWithDisplayDesc.getDisplayDescription()).toBe('User-friendly description');
143
- expect(ScriptWithoutDisplayDesc.getDisplayDescription()).toBe('Technical description');
144
- });
145
- });
146
-
147
56
  describe('Constructor', () => {
148
57
  it('should initialize with default values', () => {
149
58
  const script = new AdminScriptBase();
@@ -202,139 +202,6 @@ describe('ScriptRunner', () => {
202
202
  });
203
203
  });
204
204
 
205
- describe('dry-run mode', () => {
206
- it('should return preview without executing script', async () => {
207
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
208
-
209
- const result = await runner.execute('test-script', { foo: 'bar' }, {
210
- trigger: 'MANUAL',
211
- dryRun: true,
212
- });
213
-
214
- expect(result.dryRun).toBe(true);
215
- expect(result.status).toBe('DRY_RUN_VALID');
216
- expect(result.scriptName).toBe('test-script');
217
- expect(result.preview.script.name).toBe('test-script');
218
- expect(result.preview.script.version).toBe('1.0.0');
219
- expect(result.preview.input).toEqual({ foo: 'bar' });
220
- expect(result.message).toContain('validation passed');
221
-
222
- // Should NOT create execution record or call commands
223
- expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
224
- expect(mockCommands.updateAdminProcessState).not.toHaveBeenCalled();
225
- expect(mockCommands.completeAdminProcess).not.toHaveBeenCalled();
226
- });
227
-
228
- it('should validate required parameters in dry-run', async () => {
229
- class SchemaScript extends AdminScriptBase {
230
- static Definition = {
231
- name: 'schema-script',
232
- version: '1.0.0',
233
- description: 'Script with schema',
234
- inputSchema: {
235
- type: 'object',
236
- required: ['requiredParam'],
237
- properties: {
238
- requiredParam: { type: 'string' },
239
- optionalParam: { type: 'number' },
240
- },
241
- },
242
- };
243
-
244
- async execute() {
245
- return {};
246
- }
247
- }
248
-
249
- scriptFactory.register(SchemaScript);
250
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
251
-
252
- // Missing required parameter
253
- const result = await runner.execute('schema-script', {}, {
254
- trigger: 'MANUAL',
255
- dryRun: true,
256
- });
257
-
258
- expect(result.status).toBe('DRY_RUN_INVALID');
259
- expect(result.preview.validation.valid).toBe(false);
260
- expect(result.preview.validation.errors).toContain('Missing required parameter: requiredParam');
261
- });
262
-
263
- it('should validate parameter types in dry-run', async () => {
264
- class TypedScript extends AdminScriptBase {
265
- static Definition = {
266
- name: 'typed-script',
267
- version: '1.0.0',
268
- description: 'Script with typed params',
269
- inputSchema: {
270
- type: 'object',
271
- properties: {
272
- count: { type: 'integer' },
273
- name: { type: 'string' },
274
- enabled: { type: 'boolean' },
275
- },
276
- },
277
- };
278
-
279
- async execute() {
280
- return {};
281
- }
282
- }
283
-
284
- scriptFactory.register(TypedScript);
285
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
286
-
287
- const result = await runner.execute('typed-script', {
288
- count: 'not-a-number',
289
- name: 123,
290
- enabled: 'true',
291
- }, {
292
- trigger: 'MANUAL',
293
- dryRun: true,
294
- });
295
-
296
- expect(result.status).toBe('DRY_RUN_INVALID');
297
- expect(result.preview.validation.errors).toHaveLength(3);
298
- });
299
-
300
- it('should pass validation with correct parameters', async () => {
301
- class ValidScript extends AdminScriptBase {
302
- static Definition = {
303
- name: 'valid-script',
304
- version: '1.0.0',
305
- description: 'Script for validation',
306
- inputSchema: {
307
- type: 'object',
308
- required: ['name'],
309
- properties: {
310
- name: { type: 'string' },
311
- count: { type: 'integer' },
312
- },
313
- },
314
- };
315
-
316
- async execute() {
317
- return {};
318
- }
319
- }
320
-
321
- scriptFactory.register(ValidScript);
322
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
323
-
324
- const result = await runner.execute('valid-script', {
325
- name: 'test',
326
- count: 42,
327
- }, {
328
- trigger: 'MANUAL',
329
- dryRun: true,
330
- });
331
-
332
- expect(result.status).toBe('DRY_RUN_VALID');
333
- expect(result.preview.validation.valid).toBe(true);
334
- expect(result.preview.validation.errors).toHaveLength(0);
335
- });
336
- });
337
-
338
205
  describe('createScriptRunner()', () => {
339
206
  it('should create runner with default factory', () => {
340
207
  const runner = createScriptRunner();
@@ -0,0 +1,196 @@
1
+ const { validateScriptInput, validateParams, validateType } = require('../validate-script-input');
2
+ const { ScriptFactory } = require('../script-factory');
3
+ const { AdminScriptBase } = require('../admin-script-base');
4
+
5
+ describe('validateScriptInput', () => {
6
+ let scriptFactory;
7
+
8
+ class TestScript extends AdminScriptBase {
9
+ static Definition = {
10
+ name: 'test-script',
11
+ version: '1.0.0',
12
+ description: 'Test script',
13
+ config: {
14
+ requireIntegrationInstance: false,
15
+ },
16
+ };
17
+
18
+ async execute(params) {
19
+ return { success: true, params };
20
+ }
21
+ }
22
+
23
+ class SchemaScript extends AdminScriptBase {
24
+ static Definition = {
25
+ name: 'schema-script',
26
+ version: '1.0.0',
27
+ description: 'Script with schema',
28
+ inputSchema: {
29
+ type: 'object',
30
+ required: ['requiredParam'],
31
+ properties: {
32
+ requiredParam: { type: 'string' },
33
+ optionalParam: { type: 'number' },
34
+ },
35
+ },
36
+ };
37
+
38
+ async execute() {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ class TypedScript extends AdminScriptBase {
44
+ static Definition = {
45
+ name: 'typed-script',
46
+ version: '1.0.0',
47
+ description: 'Script with typed params',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ count: { type: 'integer' },
52
+ name: { type: 'string' },
53
+ enabled: { type: 'boolean' },
54
+ },
55
+ },
56
+ };
57
+
58
+ async execute() {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ beforeEach(() => {
64
+ scriptFactory = new ScriptFactory([TestScript, SchemaScript, TypedScript]);
65
+ });
66
+
67
+ describe('validateScriptInput()', () => {
68
+ it('should return VALID for script without schema', () => {
69
+ const result = validateScriptInput(scriptFactory, 'test-script', { foo: 'bar' });
70
+
71
+ expect(result.status).toBe('VALID');
72
+ expect(result.scriptName).toBe('test-script');
73
+ expect(result.preview.script.name).toBe('test-script');
74
+ expect(result.preview.script.version).toBe('1.0.0');
75
+ expect(result.preview.input).toEqual({ foo: 'bar' });
76
+ expect(result.message).toContain('Validation passed');
77
+ });
78
+
79
+ it('should return INVALID when required parameters are missing', () => {
80
+ const result = validateScriptInput(scriptFactory, 'schema-script', {});
81
+
82
+ expect(result.status).toBe('INVALID');
83
+ expect(result.preview.validation.valid).toBe(false);
84
+ expect(result.preview.validation.errors).toContain('Missing required parameter: requiredParam');
85
+ });
86
+
87
+ it('should return INVALID for wrong parameter types', () => {
88
+ const result = validateScriptInput(scriptFactory, 'typed-script', {
89
+ count: 'not-a-number',
90
+ name: 123,
91
+ enabled: 'true',
92
+ });
93
+
94
+ expect(result.status).toBe('INVALID');
95
+ expect(result.preview.validation.errors).toHaveLength(3);
96
+ });
97
+
98
+ it('should return VALID with correct parameters', () => {
99
+ const result = validateScriptInput(scriptFactory, 'schema-script', {
100
+ requiredParam: 'hello',
101
+ optionalParam: 42,
102
+ });
103
+
104
+ expect(result.status).toBe('VALID');
105
+ expect(result.preview.validation.valid).toBe(true);
106
+ expect(result.preview.validation.errors).toHaveLength(0);
107
+ });
108
+
109
+ it('should include inputSchema in preview', () => {
110
+ const result = validateScriptInput(scriptFactory, 'schema-script', {
111
+ requiredParam: 'test',
112
+ });
113
+
114
+ expect(result.preview.inputSchema).toEqual({
115
+ type: 'object',
116
+ required: ['requiredParam'],
117
+ properties: {
118
+ requiredParam: { type: 'string' },
119
+ optionalParam: { type: 'number' },
120
+ },
121
+ });
122
+ });
123
+
124
+ it('should return null inputSchema when script has no schema', () => {
125
+ const result = validateScriptInput(scriptFactory, 'test-script', {});
126
+
127
+ expect(result.preview.inputSchema).toBeNull();
128
+ });
129
+ });
130
+
131
+ describe('validateParams()', () => {
132
+ it('should return valid when no schema defined', () => {
133
+ const result = validateParams({ name: 'test' }, { anything: 'goes' });
134
+
135
+ expect(result.valid).toBe(true);
136
+ expect(result.errors).toHaveLength(0);
137
+ });
138
+
139
+ it('should check required fields', () => {
140
+ const definition = {
141
+ inputSchema: {
142
+ type: 'object',
143
+ required: ['a', 'b'],
144
+ properties: {
145
+ a: { type: 'string' },
146
+ b: { type: 'string' },
147
+ },
148
+ },
149
+ };
150
+
151
+ const result = validateParams(definition, { a: 'yes' });
152
+
153
+ expect(result.valid).toBe(false);
154
+ expect(result.errors).toContain('Missing required parameter: b');
155
+ });
156
+ });
157
+
158
+ describe('validateType()', () => {
159
+ it('should validate integer type', () => {
160
+ expect(validateType('x', 42, { type: 'integer' })).toBeNull();
161
+ expect(validateType('x', 3.14, { type: 'integer' })).toContain('must be an integer');
162
+ expect(validateType('x', 'foo', { type: 'integer' })).toContain('must be an integer');
163
+ });
164
+
165
+ it('should validate number type', () => {
166
+ expect(validateType('x', 3.14, { type: 'number' })).toBeNull();
167
+ expect(validateType('x', 42, { type: 'number' })).toBeNull();
168
+ expect(validateType('x', 'foo', { type: 'number' })).toContain('must be a number');
169
+ });
170
+
171
+ it('should validate string type', () => {
172
+ expect(validateType('x', 'hello', { type: 'string' })).toBeNull();
173
+ expect(validateType('x', 123, { type: 'string' })).toContain('must be a string');
174
+ });
175
+
176
+ it('should validate boolean type', () => {
177
+ expect(validateType('x', true, { type: 'boolean' })).toBeNull();
178
+ expect(validateType('x', 'true', { type: 'boolean' })).toContain('must be a boolean');
179
+ });
180
+
181
+ it('should validate array type', () => {
182
+ expect(validateType('x', [1, 2], { type: 'array' })).toBeNull();
183
+ expect(validateType('x', 'not-array', { type: 'array' })).toContain('must be an array');
184
+ });
185
+
186
+ it('should validate object type', () => {
187
+ expect(validateType('x', { a: 1 }, { type: 'object' })).toBeNull();
188
+ expect(validateType('x', [1, 2], { type: 'object' })).toContain('must be an object');
189
+ expect(validateType('x', 'string', { type: 'object' })).toContain('must be an object');
190
+ });
191
+
192
+ it('should return null when no type specified', () => {
193
+ expect(validateType('x', 'anything', {})).toBeNull();
194
+ });
195
+ });
196
+ });
@@ -1,5 +1,20 @@
1
1
  const { QueuerUtil } = require('@friggframework/core/queues');
2
2
 
3
+ /**
4
+ * AdminScriptContext - Execution environment for admin scripts
5
+ *
6
+ * Provides a controlled surface area for scripts to interact with
7
+ * the Frigg platform. Unique capabilities vs direct repo access:
8
+ *
9
+ * - **Admin bypass**: `instantiate()` passes `_isAdminContext: true` to
10
+ * skip user-ownership checks when loading integration instances
11
+ * - **Script chaining**: `queueScript()` / `queueScriptBatch()` let scripts
12
+ * enqueue follow-up work with parent execution tracking
13
+ * - **Execution-scoped logging**: `log()` collects structured entries tied
14
+ * to the current execution for post-run inspection
15
+ * - **Lazy-loaded repositories**: Repos are exposed directly as getters
16
+ * so scripts can query any data they need without wrapper indirection
17
+ */
3
18
  class AdminScriptContext {
4
19
  constructor(params = {}) {
5
20
  this.executionId = params.executionId || null;
@@ -13,7 +28,6 @@ class AdminScriptContext {
13
28
  this._userRepository = null;
14
29
  this._moduleRepository = null;
15
30
  this._credentialRepository = null;
16
- this._adminProcessRepository = null;
17
31
  }
18
32
 
19
33
  // ==================== LAZY-LOADED REPOSITORIES ====================
@@ -50,76 +64,6 @@ class AdminScriptContext {
50
64
  return this._credentialRepository;
51
65
  }
52
66
 
53
- get adminProcessRepository() {
54
- if (!this._adminProcessRepository) {
55
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
56
- this._adminProcessRepository = createAdminProcessRepository();
57
- }
58
- return this._adminProcessRepository;
59
- }
60
-
61
- // ==================== INTEGRATION QUERIES ====================
62
-
63
- async listIntegrations(filter = {}) {
64
- if (filter.userId) {
65
- return this.integrationRepository.findIntegrationsByUserId(filter.userId);
66
- }
67
- return this.integrationRepository.findIntegrations(filter);
68
- }
69
-
70
- async findIntegrationById(id) {
71
- return this.integrationRepository.findIntegrationById(id);
72
- }
73
-
74
- async findIntegrationsByUserId(userId) {
75
- return this.integrationRepository.findIntegrationsByUserId(userId);
76
- }
77
-
78
- async updateIntegrationConfig(integrationId, config) {
79
- return this.integrationRepository.updateIntegrationConfig(integrationId, config);
80
- }
81
-
82
- async updateIntegrationStatus(integrationId, status) {
83
- return this.integrationRepository.updateIntegrationStatus(integrationId, status);
84
- }
85
-
86
- // ==================== USER QUERIES ====================
87
-
88
- async findUserById(userId) {
89
- return this.userRepository.findIndividualUserById(userId);
90
- }
91
-
92
- async findUserByAppUserId(appUserId) {
93
- return this.userRepository.findIndividualUserByAppUserId(appUserId);
94
- }
95
-
96
- async findUserByUsername(username) {
97
- return this.userRepository.findIndividualUserByUsername(username);
98
- }
99
-
100
- // ==================== ENTITY QUERIES ====================
101
-
102
- async listEntities(filter = {}) {
103
- if (filter.userId) {
104
- return this.moduleRepository.findEntitiesByUserId(filter.userId);
105
- }
106
- return this.moduleRepository.findEntity(filter);
107
- }
108
-
109
- async findEntityById(entityId) {
110
- return this.moduleRepository.findEntityById(entityId);
111
- }
112
-
113
- // ==================== CREDENTIAL QUERIES ====================
114
-
115
- async findCredential(filter) {
116
- return this.credentialRepository.findCredential(filter);
117
- }
118
-
119
- async updateCredential(credentialId, updates) {
120
- return this.credentialRepository.updateCredential(credentialId, updates);
121
- }
122
-
123
67
  // ==================== INTEGRATION INSTANTIATION ====================
124
68
 
125
69
  /**
@@ -187,13 +131,6 @@ class AdminScriptContext {
187
131
  timestamp: new Date().toISOString(),
188
132
  };
189
133
  this.logs.push(entry);
190
-
191
- // Persist to execution record if we have an executionId
192
- if (this.executionId) {
193
- this.adminProcessRepository.appendProcessLog(this.executionId, entry)
194
- .catch(err => console.error('Failed to persist log:', err));
195
- }
196
-
197
134
  return entry;
198
135
  }
199
136
 
@@ -25,26 +25,6 @@ class AdminScriptBase {
25
25
  },
26
26
  };
27
27
 
28
- static getName() {
29
- return this.Definition.name;
30
- }
31
-
32
- static getCurrentVersion() {
33
- return this.Definition.version;
34
- }
35
-
36
- static getDefinition() {
37
- return this.Definition;
38
- }
39
-
40
- static getDisplayLabel() {
41
- return this.Definition.display?.label || this.Definition.name;
42
- }
43
-
44
- static getDisplayDescription() {
45
- return this.Definition.display?.description || this.Definition.description;
46
- }
47
-
48
28
  constructor(params = {}) {
49
29
  this.context = params.context || null;
50
30
  this.executionId = params.executionId || null;