@friggframework/serverless-plugin 2.0.0--canary.579.2d1eba8.0 → 2.0.0--canary.580.1003d8d.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.
- package/index.js +29 -4
- package/index.test.js +69 -0
- package/lib/localstack-queue-service.js +62 -4
- package/lib/localstack-queue-service.test.js +124 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -66,12 +66,37 @@ class FriggServerlessPlugin {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
extractQueueDefinitions() {
|
|
69
|
+
// Each custom.*Queue entry is the resolved QueueName. The matching
|
|
70
|
+
// CloudFormation resource (under resources.Resources) has the same
|
|
71
|
+
// logical ID and carries the Properties block we want to mirror onto
|
|
72
|
+
// LocalStack (VisibilityTimeout, MessageRetentionPeriod,
|
|
73
|
+
// RedrivePolicy, …). Deployed AWS applies those via CloudFormation;
|
|
74
|
+
// locally they'd be silently dropped and LocalStack would fall back
|
|
75
|
+
// to AWS defaults — notably a 30s VisibilityTimeout which
|
|
76
|
+
// re-delivers in-flight messages while a long-running queue worker
|
|
77
|
+
// is still processing them.
|
|
78
|
+
const resources =
|
|
79
|
+
this.serverless.service.resources &&
|
|
80
|
+
this.serverless.service.resources.Resources
|
|
81
|
+
? this.serverless.service.resources.Resources
|
|
82
|
+
: {};
|
|
83
|
+
|
|
69
84
|
return Object.keys(this.serverless.service.custom)
|
|
70
85
|
.filter((key) => key.endsWith('Queue'))
|
|
71
|
-
.map((key) =>
|
|
72
|
-
key
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
.map((key) => {
|
|
87
|
+
const resource = resources[key];
|
|
88
|
+
const properties =
|
|
89
|
+
resource &&
|
|
90
|
+
resource.Type === 'AWS::SQS::Queue' &&
|
|
91
|
+
resource.Properties
|
|
92
|
+
? resource.Properties
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
key,
|
|
96
|
+
name: this.serverless.service.custom[key],
|
|
97
|
+
...(properties ? { properties } : {}),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
75
100
|
}
|
|
76
101
|
|
|
77
102
|
createLocalStackSQSClient() {
|
package/index.test.js
CHANGED
|
@@ -183,6 +183,75 @@ describe('FriggServerlessPlugin', () => {
|
|
|
183
183
|
|
|
184
184
|
expect(queues).toEqual([]);
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
it('should attach Properties from matching CloudFormation resources', () => {
|
|
188
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
189
|
+
mockServerless.service.custom = {
|
|
190
|
+
HubspotQueue: 'svc--dev-HubspotQueue',
|
|
191
|
+
};
|
|
192
|
+
mockServerless.service.resources = {
|
|
193
|
+
Resources: {
|
|
194
|
+
HubspotQueue: {
|
|
195
|
+
Type: 'AWS::SQS::Queue',
|
|
196
|
+
Properties: {
|
|
197
|
+
QueueName: 'svc--dev-HubspotQueue',
|
|
198
|
+
VisibilityTimeout: 1800,
|
|
199
|
+
MessageRetentionPeriod: 345600,
|
|
200
|
+
RedrivePolicy: {
|
|
201
|
+
maxReceiveCount: 3,
|
|
202
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const queues = plugin.extractQueueDefinitions();
|
|
210
|
+
|
|
211
|
+
expect(queues).toHaveLength(1);
|
|
212
|
+
expect(queues[0]).toEqual({
|
|
213
|
+
key: 'HubspotQueue',
|
|
214
|
+
name: 'svc--dev-HubspotQueue',
|
|
215
|
+
properties: {
|
|
216
|
+
QueueName: 'svc--dev-HubspotQueue',
|
|
217
|
+
VisibilityTimeout: 1800,
|
|
218
|
+
MessageRetentionPeriod: 345600,
|
|
219
|
+
RedrivePolicy: {
|
|
220
|
+
maxReceiveCount: 3,
|
|
221
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should omit properties when resources.Resources is absent', () => {
|
|
228
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
229
|
+
mockServerless.service.custom = { LegacyQueue: 'legacy-queue' };
|
|
230
|
+
mockServerless.service.resources = undefined;
|
|
231
|
+
|
|
232
|
+
const queues = plugin.extractQueueDefinitions();
|
|
233
|
+
|
|
234
|
+
expect(queues).toEqual([{ key: 'LegacyQueue', name: 'legacy-queue' }]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should ignore non-SQS CloudFormation resources with matching logical IDs', () => {
|
|
238
|
+
plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
|
|
239
|
+
mockServerless.service.custom = { MisnamedQueue: 'misnamed-queue' };
|
|
240
|
+
mockServerless.service.resources = {
|
|
241
|
+
Resources: {
|
|
242
|
+
MisnamedQueue: {
|
|
243
|
+
Type: 'AWS::SNS::Topic',
|
|
244
|
+
Properties: { TopicName: 'not-an-sqs-queue' },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const queues = plugin.extractQueueDefinitions();
|
|
250
|
+
|
|
251
|
+
expect(queues).toEqual([
|
|
252
|
+
{ key: 'MisnamedQueue', name: 'misnamed-queue' },
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
186
255
|
});
|
|
187
256
|
|
|
188
257
|
describe('Hooks', () => {
|
|
@@ -2,19 +2,73 @@
|
|
|
2
2
|
* Infrastructure Service - LocalStack Queue Management
|
|
3
3
|
*
|
|
4
4
|
* Handles SQS queue creation in LocalStack for offline development.
|
|
5
|
+
*
|
|
6
|
+
* On deployed AWS, CloudFormation applies queue properties
|
|
7
|
+
* (VisibilityTimeout, MessageRetentionPeriod, RedrivePolicy) from the
|
|
8
|
+
* `Resources` block in serverless.yml. LocalStack gets those same
|
|
9
|
+
* properties here so local emulation matches production behavior —
|
|
10
|
+
* otherwise the queue defaults to a 30s VisibilityTimeout which re-
|
|
11
|
+
* delivers in-flight messages while a long-running queue worker is
|
|
12
|
+
* still processing them.
|
|
5
13
|
*/
|
|
6
14
|
class LocalStackQueueService {
|
|
7
15
|
constructor(sqsClient) {
|
|
8
16
|
this.sqs = sqsClient;
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Whitelist of CloudFormation `AWS::SQS::Queue` Properties that map
|
|
21
|
+
* onto SQS `CreateQueue` Attributes. Everything else (tags, inline
|
|
22
|
+
* refs, etc.) is dropped on the way to LocalStack.
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
static PROPERTY_ATTRIBUTE_KEYS = [
|
|
26
|
+
'DelaySeconds',
|
|
27
|
+
'MaximumMessageSize',
|
|
28
|
+
'MessageRetentionPeriod',
|
|
29
|
+
'ReceiveMessageWaitTimeSeconds',
|
|
30
|
+
'VisibilityTimeout',
|
|
31
|
+
'RedrivePolicy',
|
|
32
|
+
'RedriveAllowPolicy',
|
|
33
|
+
'KmsMasterKeyId',
|
|
34
|
+
'KmsDataKeyReusePeriodSeconds',
|
|
35
|
+
'SqsManagedSseEnabled',
|
|
36
|
+
'FifoQueue',
|
|
37
|
+
'ContentBasedDeduplication',
|
|
38
|
+
'DeduplicationScope',
|
|
39
|
+
'FifoThroughputLimit',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Serialize a CloudFormation `Properties` object into the
|
|
44
|
+
* `Attributes` shape the SQS `CreateQueue` API accepts (string
|
|
45
|
+
* values only; object values like `RedrivePolicy` get JSON-encoded).
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
_propertiesToAttributes(properties = {}) {
|
|
49
|
+
const attributes = {};
|
|
50
|
+
for (const key of LocalStackQueueService.PROPERTY_ATTRIBUTE_KEYS) {
|
|
51
|
+
const value = properties[key];
|
|
52
|
+
if (value === undefined || value === null) continue;
|
|
53
|
+
attributes[key] =
|
|
54
|
+
typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
55
|
+
}
|
|
56
|
+
return attributes;
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
/**
|
|
12
60
|
* @param {string} queueName - Name of queue to create
|
|
61
|
+
* @param {Object} [attributes] - SQS CreateQueue Attributes (already
|
|
62
|
+
* stringified); missing/empty attributes fall back to AWS defaults.
|
|
13
63
|
* @returns {Promise<string>} Queue URL
|
|
14
64
|
*/
|
|
15
|
-
async createQueue(queueName) {
|
|
65
|
+
async createQueue(queueName, attributes) {
|
|
16
66
|
return new Promise((resolve, reject) => {
|
|
17
|
-
|
|
67
|
+
const params = { QueueName: queueName };
|
|
68
|
+
if (attributes && Object.keys(attributes).length > 0) {
|
|
69
|
+
params.Attributes = attributes;
|
|
70
|
+
}
|
|
71
|
+
this.sqs.createQueue(params, (err, data) => {
|
|
18
72
|
if (err) {
|
|
19
73
|
reject(new Error(`Failed to create queue ${queueName}: ${err.message}`));
|
|
20
74
|
} else {
|
|
@@ -25,13 +79,17 @@ class LocalStackQueueService {
|
|
|
25
79
|
}
|
|
26
80
|
|
|
27
81
|
/**
|
|
28
|
-
* @param {Array<{key: string, name: string}>} queues
|
|
82
|
+
* @param {Array<{key: string, name: string, properties?: Object}>} queues
|
|
83
|
+
* Queue definitions. `properties` mirrors the CloudFormation
|
|
84
|
+
* `AWS::SQS::Queue` Properties block; extracted by the plugin from
|
|
85
|
+
* `serverless.service.resources.Resources`.
|
|
29
86
|
* @returns {Promise<Array<{key: string, url: string}>>} Created queues with URLs
|
|
30
87
|
*/
|
|
31
88
|
async createQueues(queues) {
|
|
32
89
|
const results = await Promise.all(
|
|
33
90
|
queues.map(async (queue) => {
|
|
34
|
-
const
|
|
91
|
+
const attributes = this._propertiesToAttributes(queue.properties);
|
|
92
|
+
const url = await this.createQueue(queue.name, attributes);
|
|
35
93
|
console.log(`Queue ${queue.name} created successfully. URL: ${url}`);
|
|
36
94
|
return { key: queue.key, url };
|
|
37
95
|
})
|
|
@@ -27,6 +27,41 @@ describe('LocalStackQueueService', () => {
|
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it('should pass Attributes when provided', async () => {
|
|
31
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
32
|
+
callback(null, { QueueUrl: 'url' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await service.createQueue('test-queue', {
|
|
36
|
+
VisibilityTimeout: '1800',
|
|
37
|
+
MessageRetentionPeriod: '345600',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(mockSQS.createQueue).toHaveBeenCalledWith(
|
|
41
|
+
{
|
|
42
|
+
QueueName: 'test-queue',
|
|
43
|
+
Attributes: {
|
|
44
|
+
VisibilityTimeout: '1800',
|
|
45
|
+
MessageRetentionPeriod: '345600',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
expect.any(Function)
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should omit Attributes when empty', async () => {
|
|
53
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
54
|
+
callback(null, { QueueUrl: 'url' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await service.createQueue('test-queue', {});
|
|
58
|
+
|
|
59
|
+
expect(mockSQS.createQueue).toHaveBeenCalledWith(
|
|
60
|
+
{ QueueName: 'test-queue' },
|
|
61
|
+
expect.any(Function)
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
30
65
|
it('should reject with error on failure', async () => {
|
|
31
66
|
const error = new Error('SQS Error');
|
|
32
67
|
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
@@ -39,6 +74,43 @@ describe('LocalStackQueueService', () => {
|
|
|
39
74
|
});
|
|
40
75
|
});
|
|
41
76
|
|
|
77
|
+
describe('_propertiesToAttributes', () => {
|
|
78
|
+
it('maps CloudFormation Properties onto SQS Attributes and stringifies', () => {
|
|
79
|
+
const attrs = service._propertiesToAttributes({
|
|
80
|
+
VisibilityTimeout: 1800,
|
|
81
|
+
MessageRetentionPeriod: 345600,
|
|
82
|
+
RedrivePolicy: {
|
|
83
|
+
maxReceiveCount: 3,
|
|
84
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
85
|
+
},
|
|
86
|
+
QueueName: 'should-be-dropped',
|
|
87
|
+
Tags: [{ Key: 'ignored', Value: 'yes' }],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(attrs).toEqual({
|
|
91
|
+
VisibilityTimeout: '1800',
|
|
92
|
+
MessageRetentionPeriod: '345600',
|
|
93
|
+
RedrivePolicy: JSON.stringify({
|
|
94
|
+
maxReceiveCount: 3,
|
|
95
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('skips undefined and null values', () => {
|
|
101
|
+
const attrs = service._propertiesToAttributes({
|
|
102
|
+
VisibilityTimeout: 60,
|
|
103
|
+
MessageRetentionPeriod: undefined,
|
|
104
|
+
KmsMasterKeyId: null,
|
|
105
|
+
});
|
|
106
|
+
expect(attrs).toEqual({ VisibilityTimeout: '60' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns an empty object when properties are missing', () => {
|
|
110
|
+
expect(service._propertiesToAttributes()).toEqual({});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
42
114
|
describe('createQueues', () => {
|
|
43
115
|
it('should create multiple queues and return results', async () => {
|
|
44
116
|
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
@@ -69,6 +141,58 @@ describe('LocalStackQueueService', () => {
|
|
|
69
141
|
consoleLogSpy.mockRestore();
|
|
70
142
|
});
|
|
71
143
|
|
|
144
|
+
it('forwards CloudFormation Properties as SQS Attributes', async () => {
|
|
145
|
+
const captured = [];
|
|
146
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
147
|
+
captured.push(params);
|
|
148
|
+
callback(null, { QueueUrl: 'url' });
|
|
149
|
+
});
|
|
150
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
151
|
+
|
|
152
|
+
await service.createQueues([
|
|
153
|
+
{
|
|
154
|
+
key: 'HubspotQueue',
|
|
155
|
+
name: 'my-service--dev-HubspotQueue',
|
|
156
|
+
properties: {
|
|
157
|
+
QueueName: 'my-service--dev-HubspotQueue',
|
|
158
|
+
VisibilityTimeout: 1800,
|
|
159
|
+
MessageRetentionPeriod: 345600,
|
|
160
|
+
RedrivePolicy: {
|
|
161
|
+
maxReceiveCount: 3,
|
|
162
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
expect(captured[0]).toEqual({
|
|
169
|
+
QueueName: 'my-service--dev-HubspotQueue',
|
|
170
|
+
Attributes: {
|
|
171
|
+
VisibilityTimeout: '1800',
|
|
172
|
+
MessageRetentionPeriod: '345600',
|
|
173
|
+
RedrivePolicy: JSON.stringify({
|
|
174
|
+
maxReceiveCount: 3,
|
|
175
|
+
deadLetterTargetArn: 'arn:aws:sqs:us-east-1:x:dlq',
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('creates queues with defaults when properties are missing (back-compat)', async () => {
|
|
182
|
+
const captured = [];
|
|
183
|
+
mockSQS.createQueue.mockImplementation((params, callback) => {
|
|
184
|
+
captured.push(params);
|
|
185
|
+
callback(null, { QueueUrl: 'url' });
|
|
186
|
+
});
|
|
187
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
188
|
+
|
|
189
|
+
await service.createQueues([
|
|
190
|
+
{ key: 'LegacyQueue', name: 'legacy-queue' },
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
expect(captured[0]).toEqual({ QueueName: 'legacy-queue' });
|
|
194
|
+
});
|
|
195
|
+
|
|
72
196
|
it('should handle empty queue array', async () => {
|
|
73
197
|
const results = await service.createQueues([]);
|
|
74
198
|
expect(results).toEqual([]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/serverless-plugin",
|
|
3
|
-
"version": "2.0.0--canary.
|
|
3
|
+
"version": "2.0.0--canary.580.1003d8d.0",
|
|
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": "1003d8deff8f587be3cd9210f1c35c5dc92531be"
|
|
15
15
|
}
|