@friggframework/serverless-plugin 2.0.0-next.47 → 2.0.0-next.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +86 -173
- package/index.test.js +178 -258
- package/lib/esbuild-directory-manager.js +27 -0
- package/lib/esbuild-directory-manager.test.js +65 -0
- package/lib/localstack-queue-service.js +44 -0
- package/lib/localstack-queue-service.test.js +94 -0
- package/lib/queue-environment-mapper.js +44 -0
- package/lib/queue-environment-mapper.test.js +74 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,208 +1,121 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { QueueEnvironmentMapper } = require('./lib/queue-environment-mapper');
|
|
2
|
+
const { LocalStackQueueService } = require('./lib/localstack-queue-service');
|
|
3
|
+
const { EsbuildDirectoryManager } = require('./lib/esbuild-directory-manager');
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
* Frigg Serverless Plugin - Handles AWS discovery and local development setup
|
|
5
|
-
*/
|
|
6
5
|
class FriggServerlessPlugin {
|
|
7
|
-
/**
|
|
8
|
-
* Creates an instance of FriggServerlessPlugin
|
|
9
|
-
* @param {Object} serverless - Serverless framework instance
|
|
10
|
-
* @param {Object} options - Plugin options
|
|
11
|
-
*/
|
|
12
6
|
constructor(serverless, options) {
|
|
13
7
|
this.serverless = serverless;
|
|
14
8
|
this.options = options;
|
|
15
|
-
this.provider = serverless.getProvider(
|
|
9
|
+
this.provider = serverless.getProvider('aws');
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const basePath = serverless.config.servicePath || process.cwd();
|
|
14
|
+
|
|
15
|
+
const dirManager = new EsbuildDirectoryManager(fs, path);
|
|
16
|
+
try {
|
|
17
|
+
const esbuildDir = dirManager.ensureDirectory(basePath);
|
|
18
|
+
console.log(`✓ Frigg plugin created ${esbuildDir}`);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`⚠️ Failed to create esbuild directory:`, error.message);
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
this.hooks = {
|
|
17
24
|
initialize: () => this.init(),
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
'before:package:initialize': () => this.beforePackageInitialize(),
|
|
26
|
+
'after:package:package': () => this.afterPackage(),
|
|
27
|
+
'before:deploy:deploy': () => this.beforeDeploy(),
|
|
21
28
|
};
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
* Asynchronous initialization for offline mode
|
|
25
|
-
* Creates SQS queues in localstack for local development
|
|
26
|
-
* @returns {Promise<void>}
|
|
27
|
-
*/
|
|
30
|
+
|
|
28
31
|
async asyncInit() {
|
|
29
|
-
this.serverless.cli.log(
|
|
30
|
-
console.log(
|
|
31
|
-
|
|
32
|
-
// CRITICAL: Create .esbuild/.serverless directory before serverless-esbuild needs it
|
|
33
|
-
// This prevents ENOENT errors during packaging
|
|
32
|
+
this.serverless.cli.log('Initializing Frigg Serverless Plugin...');
|
|
33
|
+
console.log('Hello from Frigg Serverless Plugin!');
|
|
34
|
+
|
|
34
35
|
const fs = require('fs');
|
|
35
36
|
const path = require('path');
|
|
36
|
-
const
|
|
37
|
+
const basePath = this.serverless.config.servicePath || process.cwd();
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log("Running in offline mode. Making queues!");
|
|
45
|
-
const queues = Object.keys(this.serverless.service.custom)
|
|
46
|
-
.filter((key) => key.endsWith("Queue"))
|
|
47
|
-
.map((key) => {
|
|
48
|
-
return {
|
|
49
|
-
key,
|
|
50
|
-
name: this.serverless.service.custom[key],
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
console.log("Queues to be created:", queues);
|
|
54
|
-
|
|
55
|
-
const AWS = require("aws-sdk");
|
|
56
|
-
|
|
57
|
-
const endpointUrl = process.env.AWS_ENDPOINT || "http://localhost:4566"; // LocalStack SQS endpoint
|
|
58
|
-
const region = process.env.AWS_REGION || "us-east-1";
|
|
59
|
-
const accessKeyId = process.env.AWS_ACCESS_KEY_ID || "root"; // LocalStack default
|
|
60
|
-
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || "root"; // LocalStack default
|
|
61
|
-
|
|
62
|
-
// Configure AWS SDK for LocalStack
|
|
63
|
-
AWS.config.update({
|
|
64
|
-
region: region,
|
|
65
|
-
endpoint: endpointUrl,
|
|
66
|
-
accessKeyId: accessKeyId,
|
|
67
|
-
secretAccessKey: secretAccessKey,
|
|
68
|
-
s3ForcePathStyle: true, // Required for LocalStack
|
|
69
|
-
sslEnabled: false, // Disable SSL for LocalStack
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const sqs = new AWS.SQS({
|
|
73
|
-
sslEnabled: false, // Disable SSL validation for LocalStack
|
|
74
|
-
});
|
|
75
|
-
// Find the environment variables that we need to override and create an easy map
|
|
76
|
-
const environmentMap = {};
|
|
77
|
-
const environment = this.serverless.service.provider.environment;
|
|
78
|
-
|
|
79
|
-
for (const [key, value] of Object.entries(environment)) {
|
|
80
|
-
if (typeof value === "object" && value.Ref) {
|
|
81
|
-
environmentMap[value.Ref] = key;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const queueCreationPromises = queues.map((queue) => {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
const params = {
|
|
88
|
-
QueueName: queue.name,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
sqs.createQueue(params, (err, data) => {
|
|
92
|
-
if (err) {
|
|
93
|
-
console.error(
|
|
94
|
-
`Error creating queue ${queue.name}: ${err.message}`
|
|
95
|
-
);
|
|
96
|
-
reject(err);
|
|
97
|
-
} else {
|
|
98
|
-
const queueUrl = data.QueueUrl;
|
|
99
|
-
console.log(
|
|
100
|
-
`Queue ${queue.name} created successfully. URL: ${queueUrl}`
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const environmentKey = environmentMap[queue.key];
|
|
104
|
-
this.serverless.extendConfiguration(
|
|
105
|
-
["provider", "environment", environmentKey],
|
|
106
|
-
queueUrl
|
|
107
|
-
);
|
|
108
|
-
console.log(`Set ${environmentKey} to ${queueUrl}`);
|
|
109
|
-
resolve(queueUrl);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await Promise.all(queueCreationPromises);
|
|
39
|
+
const dirManager = new EsbuildDirectoryManager(fs, path);
|
|
40
|
+
const esbuildDir = dirManager.ensureDirectory(basePath);
|
|
41
|
+
|
|
42
|
+
if (this.serverless.processedInput.commands.includes('offline')) {
|
|
43
|
+
console.log('Running in offline mode. Making queues!');
|
|
44
|
+
await this.setupOfflineQueues();
|
|
116
45
|
} else {
|
|
117
|
-
console.log(
|
|
46
|
+
console.log('Running in online mode, doing nothing');
|
|
118
47
|
}
|
|
119
48
|
}
|
|
120
49
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
50
|
+
async setupOfflineQueues() {
|
|
51
|
+
const queues = this.extractQueueDefinitions();
|
|
52
|
+
console.log('Queues to be created:', queues);
|
|
53
|
+
|
|
54
|
+
const sqsClient = this.createLocalStackSQSClient();
|
|
55
|
+
const queueService = new LocalStackQueueService(sqsClient);
|
|
56
|
+
const mapper = new QueueEnvironmentMapper();
|
|
57
|
+
|
|
58
|
+
const environmentMap = mapper.createMapping(queues);
|
|
59
|
+
const createdQueues = await queueService.createQueues(queues);
|
|
60
|
+
|
|
61
|
+
createdQueues.forEach(({ key, url }) => {
|
|
62
|
+
const envKey = mapper.getEnvironmentKey(key, environmentMap);
|
|
63
|
+
this.serverless.extendConfiguration(['provider', 'environment', envKey], url);
|
|
64
|
+
console.log(`Set ${envKey} to ${url}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
extractQueueDefinitions() {
|
|
69
|
+
return Object.keys(this.serverless.service.custom)
|
|
70
|
+
.filter((key) => key.endsWith('Queue'))
|
|
71
|
+
.map((key) => ({
|
|
72
|
+
key,
|
|
73
|
+
name: this.serverless.service.custom[key],
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
createLocalStackSQSClient() {
|
|
78
|
+
const AWS = require('aws-sdk');
|
|
79
|
+
|
|
80
|
+
AWS.config.update({
|
|
81
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
82
|
+
endpoint: process.env.AWS_ENDPOINT || 'http://localhost:4566',
|
|
83
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'root',
|
|
84
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'root',
|
|
85
|
+
s3ForcePathStyle: true,
|
|
86
|
+
sslEnabled: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return new AWS.SQS({ sslEnabled: false });
|
|
90
|
+
}
|
|
91
|
+
|
|
126
92
|
async beforePackageInitialize() {
|
|
127
|
-
|
|
128
|
-
// This hook remains for potential future use or other pre-package tasks
|
|
129
|
-
this.serverless.cli.log("Frigg Serverless Plugin: Pre-package hook");
|
|
93
|
+
this.serverless.cli.log('Frigg Serverless Plugin: Pre-package hook');
|
|
130
94
|
|
|
131
|
-
// Ensure .esbuild/.serverless directory exists to prevent ENOENT errors
|
|
132
|
-
// serverless-esbuild may try to access this directory during packaging
|
|
133
95
|
const fs = require('fs');
|
|
134
96
|
const path = require('path');
|
|
135
|
-
const
|
|
97
|
+
const basePath = this.serverless.config.servicePath || process.cwd();
|
|
136
98
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
99
|
+
const dirManager = new EsbuildDirectoryManager(fs, path);
|
|
100
|
+
dirManager.ensureDirectory(basePath);
|
|
140
101
|
}
|
|
141
102
|
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Initialization hook - runs very early, before packaging
|
|
145
|
-
* Create .esbuild/.serverless directory to prevent ENOENT errors
|
|
146
|
-
*/
|
|
147
103
|
init() {
|
|
148
|
-
// Ensure .esbuild/.serverless directory exists to prevent ENOENT errors
|
|
149
|
-
// serverless-esbuild may try to access this directory during packaging
|
|
150
104
|
const fs = require('fs');
|
|
151
105
|
const path = require('path');
|
|
152
|
-
const
|
|
106
|
+
const basePath = this.serverless.config.servicePath || process.cwd();
|
|
153
107
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
108
|
+
const dirManager = new EsbuildDirectoryManager(fs, path);
|
|
109
|
+
const esbuildDir = dirManager.ensureDirectory(basePath);
|
|
110
|
+
console.log(`Created ${esbuildDir} directory for serverless-esbuild`);
|
|
158
111
|
}
|
|
159
|
-
|
|
160
|
-
* Hook that runs after serverless package
|
|
161
|
-
*/
|
|
112
|
+
|
|
162
113
|
afterPackage() {
|
|
163
|
-
console.log(
|
|
164
|
-
// // const queues = Object.keys(infrastructure.custom)
|
|
165
|
-
// // .filter((key) => key.endsWith('Queue'))
|
|
166
|
-
// // .map((key) => infrastructure.custom[key]);
|
|
167
|
-
// // console.log('Queues to be created:', queues);
|
|
168
|
-
// //
|
|
169
|
-
// // const endpointUrl = 'http://localhost:4566'; // Assuming localstack is running on port 4
|
|
170
|
-
// // const region = 'us-east-1';
|
|
171
|
-
// // const command = 'aws';
|
|
172
|
-
// // queues.forEach((queue) => {
|
|
173
|
-
// // const args = [
|
|
174
|
-
// // '--endpoint-url',
|
|
175
|
-
// // endpointUrl,
|
|
176
|
-
// // 'sqs',
|
|
177
|
-
// // 'create-queue',
|
|
178
|
-
// // '--queue-name',
|
|
179
|
-
// // queue,
|
|
180
|
-
// // '--region',
|
|
181
|
-
// // region,
|
|
182
|
-
// // '--output',
|
|
183
|
-
// // 'table',
|
|
184
|
-
// // ];
|
|
185
|
-
// //
|
|
186
|
-
// // const childProcess = spawn(command, args, {
|
|
187
|
-
// // cwd: backendPath,
|
|
188
|
-
// // stdio: 'inherit',
|
|
189
|
-
// // });
|
|
190
|
-
// // childProcess.on('error', (error) => {
|
|
191
|
-
// // console.error(`Error executing command: ${error.message}`);
|
|
192
|
-
// // });
|
|
193
|
-
// //
|
|
194
|
-
// // childProcess.on('close', (code) => {
|
|
195
|
-
// // if (code !== 0) {
|
|
196
|
-
// // console.log(`Child process exited with code ${code}`);
|
|
197
|
-
// // }
|
|
198
|
-
// // });
|
|
199
|
-
// });
|
|
114
|
+
console.log('After package hook called');
|
|
200
115
|
}
|
|
201
|
-
|
|
202
|
-
* Hook that runs before serverless deploy
|
|
203
|
-
*/
|
|
116
|
+
|
|
204
117
|
beforeDeploy() {
|
|
205
|
-
console.log(
|
|
118
|
+
console.log('Before deploy hook called');
|
|
206
119
|
}
|
|
207
120
|
}
|
|
208
121
|
|
package/index.test.js
CHANGED
|
@@ -1,307 +1,227 @@
|
|
|
1
1
|
const FriggServerlessPlugin = require('./index');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
jest.mock('
|
|
3
|
+
jest.mock('./lib/queue-environment-mapper');
|
|
4
|
+
jest.mock('./lib/localstack-queue-service');
|
|
5
|
+
jest.mock('./lib/esbuild-directory-manager');
|
|
6
|
+
|
|
7
|
+
const { QueueEnvironmentMapper } = require('./lib/queue-environment-mapper');
|
|
8
|
+
const { LocalStackQueueService } = require('./lib/localstack-queue-service');
|
|
9
|
+
const { EsbuildDirectoryManager } = require('./lib/esbuild-directory-manager');
|
|
7
10
|
|
|
8
11
|
describe('FriggServerlessPlugin', () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
jest.clearAllMocks();
|
|
12
|
+
let plugin;
|
|
13
|
+
let mockServerless;
|
|
14
|
+
let mockOptions;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockServerless = {
|
|
18
|
+
config: { servicePath: '/test/path' },
|
|
19
|
+
cli: { log: jest.fn() },
|
|
20
|
+
service: {
|
|
21
|
+
custom: {},
|
|
22
|
+
provider: { environment: {} },
|
|
23
|
+
},
|
|
24
|
+
processedInput: { commands: [] },
|
|
25
|
+
getProvider: jest.fn().mockReturnValue({}),
|
|
26
|
+
extendConfiguration: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
mockOptions = { stage: 'test' };
|
|
30
|
+
|
|
31
|
+
EsbuildDirectoryManager.mockImplementation(() => ({
|
|
32
|
+
ensureDirectory: jest.fn().mockReturnValue('/test/path/.esbuild/.serverless'),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Constructor', () => {
|
|
39
|
+
it('should initialize with serverless instance and options', () => {
|
|
40
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
41
|
+
|
|
42
|
+
expect(plugin.serverless).toBe(mockServerless);
|
|
43
|
+
expect(plugin.options).toBe(mockOptions);
|
|
44
|
+
expect(plugin.hooks).toBeDefined();
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
48
|
-
|
|
49
|
-
expect(plugin.serverless).toBe(mockServerless);
|
|
50
|
-
expect(plugin.options).toBe(mockOptions);
|
|
51
|
-
expect(plugin.hooks).toBeDefined();
|
|
52
|
-
});
|
|
47
|
+
it('should register required hooks', () => {
|
|
48
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(plugin.hooks).toHaveProperty('before:package:initialize');
|
|
59
|
-
expect(plugin.hooks).toHaveProperty('after:package:package');
|
|
60
|
-
expect(plugin.hooks).toHaveProperty('before:deploy:deploy');
|
|
61
|
-
});
|
|
50
|
+
expect(plugin.hooks).toHaveProperty('initialize');
|
|
51
|
+
expect(plugin.hooks).toHaveProperty('before:package:initialize');
|
|
52
|
+
expect(plugin.hooks).toHaveProperty('after:package:package');
|
|
53
|
+
expect(plugin.hooks).toHaveProperty('before:deploy:deploy');
|
|
62
54
|
});
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
fs.existsSync.mockReturnValue(false);
|
|
70
|
-
fs.mkdirSync.mockImplementation(() => { });
|
|
71
|
-
|
|
72
|
-
// Spy on console.log
|
|
73
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
74
|
-
|
|
75
|
-
await plugin.asyncInit();
|
|
76
|
-
|
|
77
|
-
const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
|
|
78
|
-
|
|
79
|
-
// Verify directory existence check
|
|
80
|
-
expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
|
|
81
|
-
|
|
82
|
-
// Verify directory creation
|
|
83
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
|
|
84
|
-
|
|
85
|
-
// Verify success message
|
|
86
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
87
|
-
expect.stringContaining('Created')
|
|
88
|
-
);
|
|
89
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
90
|
-
expect.stringContaining('.esbuild')
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
consoleLogSpy.mockRestore();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should not create directory if it already exists', async () => {
|
|
97
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
98
|
-
|
|
99
|
-
// Mock fs.existsSync to return true (directory exists)
|
|
100
|
-
fs.existsSync.mockReturnValue(true);
|
|
101
|
-
fs.mkdirSync.mockImplementation(() => { });
|
|
102
|
-
|
|
103
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
104
|
-
|
|
105
|
-
await plugin.asyncInit();
|
|
106
|
-
|
|
107
|
-
const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
|
|
108
|
-
|
|
109
|
-
// Verify directory existence check
|
|
110
|
-
expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
|
|
111
|
-
|
|
112
|
-
// Verify directory creation was NOT called
|
|
113
|
-
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
114
|
-
|
|
115
|
-
// Verify success message was NOT logged
|
|
116
|
-
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
|
117
|
-
expect.stringContaining('Created')
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
consoleLogSpy.mockRestore();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should use process.cwd() if servicePath is not available', async () => {
|
|
124
|
-
// Remove servicePath from config
|
|
125
|
-
mockServerless.config.servicePath = undefined;
|
|
126
|
-
|
|
127
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
128
|
-
|
|
129
|
-
fs.existsSync.mockReturnValue(false);
|
|
130
|
-
fs.mkdirSync.mockImplementation(() => { });
|
|
131
|
-
|
|
132
|
-
await plugin.asyncInit();
|
|
133
|
-
|
|
134
|
-
const expectedPath = path.join(process.cwd(), '.esbuild', '.serverless');
|
|
135
|
-
|
|
136
|
-
expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
|
|
137
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
|
|
138
|
-
});
|
|
56
|
+
it('should create esbuild directory on construction', () => {
|
|
57
|
+
const mockEnsureDir = jest.fn().mockReturnValue('/test/.esbuild/.serverless');
|
|
58
|
+
EsbuildDirectoryManager.mockImplementation(() => ({
|
|
59
|
+
ensureDirectory: mockEnsureDir,
|
|
60
|
+
}));
|
|
139
61
|
|
|
140
|
-
|
|
141
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
62
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
142
63
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
146
|
-
|
|
147
|
-
await plugin.asyncInit();
|
|
148
|
-
|
|
149
|
-
// Verify plugin initialization messages
|
|
150
|
-
expect(mockServerless.cli.log).toHaveBeenCalledWith('Initializing Frigg Serverless Plugin...');
|
|
151
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('Hello from Frigg Serverless Plugin!');
|
|
152
|
-
|
|
153
|
-
consoleLogSpy.mockRestore();
|
|
154
|
-
});
|
|
64
|
+
expect(mockEnsureDir).toHaveBeenCalledWith('/test/path');
|
|
155
65
|
});
|
|
66
|
+
});
|
|
156
67
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
fs.existsSync.mockReturnValue(true);
|
|
162
|
-
|
|
163
|
-
// Not in offline mode
|
|
164
|
-
mockServerless.processedInput.commands = ['deploy'];
|
|
165
|
-
|
|
166
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
68
|
+
describe('asyncInit', () => {
|
|
69
|
+
it('should log initialization messages', async () => {
|
|
70
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
71
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
167
72
|
|
|
168
|
-
|
|
73
|
+
await plugin.asyncInit();
|
|
169
74
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect.stringContaining('offline mode')
|
|
173
|
-
);
|
|
75
|
+
expect(mockServerless.cli.log).toHaveBeenCalledWith('Initializing Frigg Serverless Plugin...');
|
|
76
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Hello from Frigg Serverless Plugin!');
|
|
174
77
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
it('should create SQS queues when in offline mode', async () => {
|
|
179
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
180
|
-
|
|
181
|
-
fs.existsSync.mockReturnValue(true);
|
|
182
|
-
|
|
183
|
-
// Set offline mode
|
|
184
|
-
mockServerless.processedInput.commands = ['offline'];
|
|
185
|
-
mockServerless.service.custom = {
|
|
186
|
-
testQueue: 'test-queue-name',
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Mock AWS SDK
|
|
190
|
-
const mockCreateQueue = jest.fn((params, callback) => {
|
|
191
|
-
callback(null, { QueueUrl: 'http://localhost:4566/queue/test-queue-name' });
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
jest.mock('aws-sdk', () => ({
|
|
195
|
-
SQS: jest.fn(() => ({
|
|
196
|
-
createQueue: mockCreateQueue,
|
|
197
|
-
})),
|
|
198
|
-
config: {
|
|
199
|
-
update: jest.fn(),
|
|
200
|
-
},
|
|
201
|
-
}));
|
|
78
|
+
consoleLogSpy.mockRestore();
|
|
79
|
+
});
|
|
202
80
|
|
|
203
|
-
|
|
81
|
+
it('should run in online mode when not offline', async () => {
|
|
82
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
83
|
+
mockServerless.processedInput.commands = ['deploy'];
|
|
204
84
|
|
|
205
|
-
|
|
85
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
206
86
|
|
|
207
|
-
|
|
208
|
-
expect.stringContaining('offline mode')
|
|
209
|
-
);
|
|
87
|
+
await plugin.asyncInit();
|
|
210
88
|
|
|
211
|
-
|
|
212
|
-
|
|
89
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Running in online mode, doing nothing');
|
|
90
|
+
consoleLogSpy.mockRestore();
|
|
213
91
|
});
|
|
214
92
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
93
|
+
it('should setup offline queues when in offline mode', async () => {
|
|
94
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
95
|
+
mockServerless.processedInput.commands = ['offline'];
|
|
96
|
+
mockServerless.service.custom = { AsanaQueue: 'test-queue' };
|
|
218
97
|
|
|
219
|
-
|
|
220
|
-
|
|
98
|
+
const mockMapper = {
|
|
99
|
+
createMapping: jest.fn().mockReturnValue({ AsanaQueue: 'ASANA_QUEUE_URL' }),
|
|
100
|
+
getEnvironmentKey: jest.fn().mockReturnValue('ASANA_QUEUE_URL'),
|
|
101
|
+
};
|
|
102
|
+
QueueEnvironmentMapper.mockImplementation(() => mockMapper);
|
|
221
103
|
|
|
222
|
-
|
|
104
|
+
const mockQueueService = {
|
|
105
|
+
createQueues: jest.fn().mockResolvedValue([
|
|
106
|
+
{ key: 'AsanaQueue', url: 'http://localhost:4566/queue' },
|
|
107
|
+
]),
|
|
108
|
+
};
|
|
109
|
+
LocalStackQueueService.mockImplementation(() => mockQueueService);
|
|
223
110
|
|
|
224
|
-
|
|
111
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
225
112
|
|
|
226
|
-
|
|
227
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
|
|
228
|
-
});
|
|
113
|
+
await plugin.asyncInit();
|
|
229
114
|
|
|
230
|
-
|
|
231
|
-
|
|
115
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Running in offline mode. Making queues!');
|
|
116
|
+
expect(mockQueueService.createQueues).toHaveBeenCalled();
|
|
117
|
+
expect(mockServerless.extendConfiguration).toHaveBeenCalledWith(
|
|
118
|
+
['provider', 'environment', 'ASANA_QUEUE_URL'],
|
|
119
|
+
'http://localhost:4566/queue'
|
|
120
|
+
);
|
|
232
121
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
122
|
+
consoleLogSpy.mockRestore();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('setupOfflineQueues', () => {
|
|
127
|
+
it('should orchestrate queue creation and environment configuration', async () => {
|
|
128
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
129
|
+
mockServerless.service.custom = {
|
|
130
|
+
AsanaQueue: 'test-asana-queue',
|
|
131
|
+
SlackQueue: 'test-slack-queue',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const mockMapper = {
|
|
135
|
+
createMapping: jest.fn().mockReturnValue({
|
|
136
|
+
AsanaQueue: 'ASANA_QUEUE_URL',
|
|
137
|
+
SlackQueue: 'SLACK_QUEUE_URL',
|
|
138
|
+
}),
|
|
139
|
+
getEnvironmentKey: jest.fn()
|
|
140
|
+
.mockReturnValueOnce('ASANA_QUEUE_URL')
|
|
141
|
+
.mockReturnValueOnce('SLACK_QUEUE_URL'),
|
|
142
|
+
};
|
|
143
|
+
QueueEnvironmentMapper.mockImplementation(() => mockMapper);
|
|
144
|
+
|
|
145
|
+
const mockQueueService = {
|
|
146
|
+
createQueues: jest.fn().mockResolvedValue([
|
|
147
|
+
{ key: 'AsanaQueue', url: 'http://localhost:4566/asana' },
|
|
148
|
+
{ key: 'SlackQueue', url: 'http://localhost:4566/slack' },
|
|
149
|
+
]),
|
|
150
|
+
};
|
|
151
|
+
LocalStackQueueService.mockImplementation(() => mockQueueService);
|
|
152
|
+
|
|
153
|
+
await plugin.setupOfflineQueues();
|
|
154
|
+
|
|
155
|
+
expect(mockMapper.createMapping).toHaveBeenCalled();
|
|
156
|
+
expect(mockQueueService.createQueues).toHaveBeenCalled();
|
|
157
|
+
expect(mockServerless.extendConfiguration).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('extractQueueDefinitions', () => {
|
|
162
|
+
it('should extract queue definitions from custom config', () => {
|
|
163
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
164
|
+
mockServerless.service.custom = {
|
|
165
|
+
AsanaQueue: 'test-asana-queue',
|
|
166
|
+
someOtherConfig: 'something-else',
|
|
167
|
+
SlackQueue: 'test-slack-queue',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const queues = plugin.extractQueueDefinitions();
|
|
171
|
+
|
|
172
|
+
expect(queues).toEqual([
|
|
173
|
+
{ key: 'AsanaQueue', name: 'test-asana-queue' },
|
|
174
|
+
{ key: 'SlackQueue', name: 'test-slack-queue' },
|
|
175
|
+
]);
|
|
239
176
|
});
|
|
240
177
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
fs.existsSync.mockReturnValue(false);
|
|
246
|
-
fs.mkdirSync.mockImplementation(() => { });
|
|
178
|
+
it('should return empty array when no queues defined', () => {
|
|
179
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
180
|
+
mockServerless.service.custom = { someConfig: 'value' };
|
|
247
181
|
|
|
248
|
-
|
|
182
|
+
const queues = plugin.extractQueueDefinitions();
|
|
249
183
|
|
|
250
|
-
|
|
184
|
+
expect(queues).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
251
187
|
|
|
252
|
-
|
|
188
|
+
describe('Hooks', () => {
|
|
189
|
+
it('should execute init hook', () => {
|
|
190
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
191
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
253
192
|
|
|
254
|
-
|
|
255
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
|
|
256
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
257
|
-
expect.stringContaining('Created')
|
|
258
|
-
);
|
|
193
|
+
plugin.init();
|
|
259
194
|
|
|
260
|
-
|
|
261
|
-
|
|
195
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
196
|
+
consoleLogSpy.mockRestore();
|
|
262
197
|
});
|
|
263
198
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
199
|
+
it('should execute beforePackageInitialize hook', async () => {
|
|
200
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
267
201
|
|
|
268
|
-
|
|
202
|
+
await plugin.beforePackageInitialize();
|
|
269
203
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('After package hook called');
|
|
273
|
-
|
|
274
|
-
consoleLogSpy.mockRestore();
|
|
275
|
-
});
|
|
204
|
+
expect(mockServerless.cli.log).toHaveBeenCalledWith('Frigg Serverless Plugin: Pre-package hook');
|
|
276
205
|
});
|
|
277
206
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
207
|
+
it('should execute afterPackage hook', () => {
|
|
208
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
209
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
283
210
|
|
|
284
|
-
|
|
211
|
+
plugin.afterPackage();
|
|
285
212
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
consoleLogSpy.mockRestore();
|
|
289
|
-
});
|
|
213
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('After package hook called');
|
|
214
|
+
consoleLogSpy.mockRestore();
|
|
290
215
|
});
|
|
291
216
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
217
|
+
it('should execute beforeDeploy hook', () => {
|
|
218
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
219
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
295
220
|
|
|
296
|
-
|
|
297
|
-
fs.mkdirSync.mockImplementation(() => {
|
|
298
|
-
throw new Error('Permission denied');
|
|
299
|
-
});
|
|
221
|
+
plugin.beforeDeploy();
|
|
300
222
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
223
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Before deploy hook called');
|
|
224
|
+
consoleLogSpy.mockRestore();
|
|
304
225
|
});
|
|
226
|
+
});
|
|
305
227
|
});
|
|
306
|
-
|
|
307
|
-
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Service - ESBuild Directory Management
|
|
3
|
+
*
|
|
4
|
+
* Ensures .esbuild/.serverless directory exists to prevent ENOENT errors.
|
|
5
|
+
*/
|
|
6
|
+
class EsbuildDirectoryManager {
|
|
7
|
+
constructor(fs, path) {
|
|
8
|
+
this.fs = fs;
|
|
9
|
+
this.path = path;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} basePath - Base service path
|
|
14
|
+
* @returns {string} Created directory path
|
|
15
|
+
*/
|
|
16
|
+
ensureDirectory(basePath) {
|
|
17
|
+
const esbuildDir = this.path.join(basePath, '.esbuild', '.serverless');
|
|
18
|
+
|
|
19
|
+
if (!this.fs.existsSync(esbuildDir)) {
|
|
20
|
+
this.fs.mkdirSync(esbuildDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return esbuildDir;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { EsbuildDirectoryManager };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { EsbuildDirectoryManager } = require('./esbuild-directory-manager');
|
|
2
|
+
|
|
3
|
+
describe('EsbuildDirectoryManager', () => {
|
|
4
|
+
let manager;
|
|
5
|
+
let mockFs;
|
|
6
|
+
let mockPath;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFs = {
|
|
10
|
+
existsSync: jest.fn(),
|
|
11
|
+
mkdirSync: jest.fn(),
|
|
12
|
+
};
|
|
13
|
+
mockPath = {
|
|
14
|
+
join: jest.fn((...args) => args.join('/')),
|
|
15
|
+
};
|
|
16
|
+
manager = new EsbuildDirectoryManager(mockFs, mockPath);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('ensureDirectory', () => {
|
|
20
|
+
it('should create directory if it does not exist', () => {
|
|
21
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
22
|
+
|
|
23
|
+
const result = manager.ensureDirectory('/test/path');
|
|
24
|
+
|
|
25
|
+
expect(mockPath.join).toHaveBeenCalledWith('/test/path', '.esbuild', '.serverless');
|
|
26
|
+
expect(mockFs.existsSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless');
|
|
27
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless', {
|
|
28
|
+
recursive: true,
|
|
29
|
+
});
|
|
30
|
+
expect(result).toBe('/test/path/.esbuild/.serverless');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should not create directory if it already exists', () => {
|
|
34
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
35
|
+
|
|
36
|
+
const result = manager.ensureDirectory('/test/path');
|
|
37
|
+
|
|
38
|
+
expect(mockFs.existsSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless');
|
|
39
|
+
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
40
|
+
expect(result).toBe('/test/path/.esbuild/.serverless');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle different base paths', () => {
|
|
44
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
45
|
+
|
|
46
|
+
manager.ensureDirectory('/different/base');
|
|
47
|
+
|
|
48
|
+
expect(mockPath.join).toHaveBeenCalledWith('/different/base', '.esbuild', '.serverless');
|
|
49
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith('/different/base/.esbuild/.serverless', {
|
|
50
|
+
recursive: true,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should propagate fs errors', () => {
|
|
55
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
56
|
+
mockFs.mkdirSync.mockImplementation(() => {
|
|
57
|
+
throw new Error('Permission denied');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(() => {
|
|
61
|
+
manager.ensureDirectory('/test/path');
|
|
62
|
+
}).toThrow('Permission denied');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Service - LocalStack Queue Management
|
|
3
|
+
*
|
|
4
|
+
* Handles SQS queue creation in LocalStack for offline development.
|
|
5
|
+
*/
|
|
6
|
+
class LocalStackQueueService {
|
|
7
|
+
constructor(sqsClient) {
|
|
8
|
+
this.sqs = sqsClient;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} queueName - Name of queue to create
|
|
13
|
+
* @returns {Promise<string>} Queue URL
|
|
14
|
+
*/
|
|
15
|
+
async createQueue(queueName) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
this.sqs.createQueue({ QueueName: queueName }, (err, data) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
reject(new Error(`Failed to create queue ${queueName}: ${err.message}`));
|
|
20
|
+
} else {
|
|
21
|
+
resolve(data.QueueUrl);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Array<{key: string, name: string}>} queues - Queue definitions
|
|
29
|
+
* @returns {Promise<Array<{key: string, url: string}>>} Created queues with URLs
|
|
30
|
+
*/
|
|
31
|
+
async createQueues(queues) {
|
|
32
|
+
const results = await Promise.all(
|
|
33
|
+
queues.map(async (queue) => {
|
|
34
|
+
const url = await this.createQueue(queue.name);
|
|
35
|
+
console.log(`Queue ${queue.name} created successfully. URL: ${url}`);
|
|
36
|
+
return { key: queue.key, url };
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { LocalStackQueueService };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { LocalStackQueueService } = require('./localstack-queue-service');
|
|
2
|
+
|
|
3
|
+
describe('LocalStackQueueService', () => {
|
|
4
|
+
let service;
|
|
5
|
+
let mockSQS;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockSQS = {
|
|
9
|
+
createQueue: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
service = new LocalStackQueueService(mockSQS);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('createQueue', () => {
|
|
15
|
+
it('should create queue and return URL on success', async () => {
|
|
16
|
+
const queueUrl = 'http://localhost:4566/000000000000/test-queue';
|
|
17
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
18
|
+
callback(null, { QueueUrl: queueUrl });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = await service.createQueue('test-queue');
|
|
22
|
+
|
|
23
|
+
expect(result).toBe(queueUrl);
|
|
24
|
+
expect(mockSQS.createQueue).toHaveBeenCalledWith(
|
|
25
|
+
{ QueueName: 'test-queue' },
|
|
26
|
+
expect.any(Function)
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject with error on failure', async () => {
|
|
31
|
+
const error = new Error('SQS Error');
|
|
32
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
33
|
+
callback(error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await expect(service.createQueue('test-queue')).rejects.toThrow(
|
|
37
|
+
'Failed to create queue test-queue: SQS Error'
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('createQueues', () => {
|
|
43
|
+
it('should create multiple queues and return results', async () => {
|
|
44
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
45
|
+
const url = `http://localhost:4566/000000000000/${params.QueueName}`;
|
|
46
|
+
callback(null, { QueueUrl: url });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
50
|
+
|
|
51
|
+
const queues = [
|
|
52
|
+
{ key: 'AsanaQueue', name: 'test-asana-queue' },
|
|
53
|
+
{ key: 'SlackQueue', name: 'test-slack-queue' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const results = await service.createQueues(queues);
|
|
57
|
+
|
|
58
|
+
expect(results).toHaveLength(2);
|
|
59
|
+
expect(results[0]).toEqual({
|
|
60
|
+
key: 'AsanaQueue',
|
|
61
|
+
url: 'http://localhost:4566/000000000000/test-asana-queue',
|
|
62
|
+
});
|
|
63
|
+
expect(results[1]).toEqual({
|
|
64
|
+
key: 'SlackQueue',
|
|
65
|
+
url: 'http://localhost:4566/000000000000/test-slack-queue',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
69
|
+
consoleLogSpy.mockRestore();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle empty queue array', async () => {
|
|
73
|
+
const results = await service.createQueues([]);
|
|
74
|
+
expect(results).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should reject if any queue creation fails', async () => {
|
|
78
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
79
|
+
if (params.QueueName === 'failing-queue') {
|
|
80
|
+
callback(new Error('Failed'));
|
|
81
|
+
} else {
|
|
82
|
+
callback(null, { QueueUrl: 'http://localhost:4566/queue' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const queues = [
|
|
87
|
+
{ key: 'SuccessQueue', name: 'success-queue' },
|
|
88
|
+
{ key: 'FailQueue', name: 'failing-queue' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
await expect(service.createQueues(queues)).rejects.toThrow('Failed to create queue failing-queue');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Service - Queue to Environment Variable Mapper
|
|
3
|
+
*
|
|
4
|
+
* Maps queue keys to environment variable names using naming convention.
|
|
5
|
+
* Pattern: AsanaQueue -> ASANA_QUEUE_URL
|
|
6
|
+
*/
|
|
7
|
+
class QueueEnvironmentMapper {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Array<{key: string, name: string}>} queues - Queue definitions
|
|
10
|
+
* @returns {Object} Map of queue keys to environment variable names
|
|
11
|
+
*/
|
|
12
|
+
createMapping(queues) {
|
|
13
|
+
const mapping = {};
|
|
14
|
+
|
|
15
|
+
queues.forEach(queue => {
|
|
16
|
+
const baseName = queue.key.replace(/Queue$/, '');
|
|
17
|
+
const envVarName = `${baseName.toUpperCase()}_QUEUE_URL`;
|
|
18
|
+
mapping[queue.key] = envVarName;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return mapping;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} queueKey - Queue key to look up
|
|
26
|
+
* @param {Object} mapping - Environment variable mapping
|
|
27
|
+
* @returns {string} Environment variable name
|
|
28
|
+
* @throws {Error} If no mapping found
|
|
29
|
+
*/
|
|
30
|
+
getEnvironmentKey(queueKey, mapping) {
|
|
31
|
+
const envKey = mapping[queueKey];
|
|
32
|
+
|
|
33
|
+
if (!envKey) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`No environment variable mapping found for queue "${queueKey}". ` +
|
|
36
|
+
`Expected pattern: {QueueName}Queue -> {QUEUENAME}_QUEUE_URL`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return envKey;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { QueueEnvironmentMapper };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { QueueEnvironmentMapper } = require('./queue-environment-mapper');
|
|
2
|
+
|
|
3
|
+
describe('QueueEnvironmentMapper', () => {
|
|
4
|
+
let mapper;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
mapper = new QueueEnvironmentMapper();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('createMapping', () => {
|
|
11
|
+
it('should map single queue key to environment variable name', () => {
|
|
12
|
+
const queues = [{ key: 'AsanaQueue', name: 'test-asana-queue' }];
|
|
13
|
+
const result = mapper.createMapping(queues);
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
AsanaQueue: 'ASANA_QUEUE_URL',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should map multiple queue keys to environment variable names', () => {
|
|
21
|
+
const queues = [
|
|
22
|
+
{ key: 'AsanaQueue', name: 'test-asana-queue' },
|
|
23
|
+
{ key: 'SlackQueue', name: 'test-slack-queue' },
|
|
24
|
+
{ key: 'HubspotQueue', name: 'test-hubspot-queue' },
|
|
25
|
+
];
|
|
26
|
+
const result = mapper.createMapping(queues);
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
AsanaQueue: 'ASANA_QUEUE_URL',
|
|
30
|
+
SlackQueue: 'SLACK_QUEUE_URL',
|
|
31
|
+
HubspotQueue: 'HUBSPOT_QUEUE_URL',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle empty queue array', () => {
|
|
36
|
+
const result = mapper.createMapping([]);
|
|
37
|
+
expect(result).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should preserve case in base name conversion', () => {
|
|
41
|
+
const queues = [{ key: 'MyCustomQueue', name: 'test-queue' }];
|
|
42
|
+
const result = mapper.createMapping(queues);
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
MyCustomQueue: 'MYCUSTOM_QUEUE_URL',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getEnvironmentKey', () => {
|
|
51
|
+
it('should return environment variable name for valid queue key', () => {
|
|
52
|
+
const mapping = { AsanaQueue: 'ASANA_QUEUE_URL' };
|
|
53
|
+
const result = mapper.getEnvironmentKey('AsanaQueue', mapping);
|
|
54
|
+
|
|
55
|
+
expect(result).toBe('ASANA_QUEUE_URL');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error for missing queue key', () => {
|
|
59
|
+
const mapping = { AsanaQueue: 'ASANA_QUEUE_URL' };
|
|
60
|
+
|
|
61
|
+
expect(() => {
|
|
62
|
+
mapper.getEnvironmentKey('InvalidQueue', mapping);
|
|
63
|
+
}).toThrow('No environment variable mapping found for queue "InvalidQueue"');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should throw error with helpful message pattern', () => {
|
|
67
|
+
const mapping = {};
|
|
68
|
+
|
|
69
|
+
expect(() => {
|
|
70
|
+
mapper.getEnvironmentKey('TestQueue', mapping);
|
|
71
|
+
}).toThrow('Expected pattern: {QueueName}Queue -> {QUEUENAME}_QUEUE_URL');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/serverless-plugin",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.49",
|
|
4
4
|
"description": "Plugin to help dynamically load frigg resources",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
|
-
"gitHead": "
|
|
14
|
+
"gitHead": "ab5b2d2e5f0a24bc4af6b3dc0ac592c358638bfa"
|
|
15
15
|
}
|