@friggframework/serverless-plugin 2.0.0--canary.579.2d1eba8.0 → 2.0.0--canary.580.5360ba8.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 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
- name: this.serverless.service.custom[key],
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
- this.sqs.createQueue({ QueueName: queueName }, (err, data) => {
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 - Queue definitions
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 url = await this.createQueue(queue.name);
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.579.2d1eba8.0",
3
+ "version": "2.0.0--canary.580.5360ba8.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": "2d1eba87ebd7c2e83b0e95a87a41eb9037417694"
14
+ "gitHead": "5360ba85bffb749770983b52583000b56923ca2e"
15
15
  }