@voxgig/build 3.1.0 → 4.0.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/build.ts CHANGED
@@ -1,496 +1,30 @@
1
- /* Copyright © 2022 Voxgig Ltd, MIT License. */
1
+ /* Copyright © 2022-2026 Voxgig Ltd, MIT License. */
2
+
3
+ // EnvLambda: generation of the Lambda deployment templates, built on the
4
+ // jostraca templating library. The templates live in env/lambda/, one file
5
+ // per output for convenience:
6
+ //
7
+ // env/lambda/srv_yml.ts gen srv.yml (Serverless function defs)
8
+ // env/lambda/srv_handler.ts handler source (one per lambda service)
9
+ // env/lambda/res_yml.ts gen res.yml (queues, dynamo, IAM role)
10
+ //
11
+ // The output is byte-identical to the pre-jostraca (<= 3.1.0) generator;
12
+ // test/fixture pins this. All generators are async (jostraca generation is
13
+ // async); existing callers that did not await still work, as the writes
14
+ // complete before the process exits.
15
+
16
+ import { srv_yml } from './env/lambda/srv_yml'
17
+ import { srv_handler } from './env/lambda/srv_handler'
18
+ import { resources_yml } from './env/lambda/res_yml'
2
19
 
3
- import Fs from 'fs'
4
- import Path from 'path'
5
-
6
- import { dive, get, pinify, camelify } from '@voxgig/util'
7
-
8
- import { MsgMetaShape } from './shape/msg'
9
- import { CoreConfShape, CloudConfShape } from './shape/conf'
10
-
11
- import { res_dynamo_yml } from './yml/res_dynamo_yml'
12
-
13
-
14
- // console.log('BUILD 1')
15
20
 
16
21
  const EnvLambda = {
17
-
18
- srv_yml: (model: any, spec: {
19
- folder: string
20
- }) => {
21
-
22
- const core = CoreConfShape(model.main.conf.core)
23
-
24
- let appname = core.name
25
- let AppName = camelify(appname)
26
-
27
- let srv_yml_path = Path.join(spec.folder, 'srv.yml')
28
-
29
- let srv_yml_prefix_path = Path.join(spec.folder, 'srv.prefix.yml')
30
- let srv_yml_suffix_path = Path.join(spec.folder, 'srv.suffix.yml')
31
-
32
- let prefixContent = Fs.existsSync(srv_yml_prefix_path) ?
33
- Fs.readFileSync(srv_yml_prefix_path) : ''
34
- let suffixContent = Fs.existsSync(srv_yml_suffix_path) ?
35
- Fs.readFileSync(srv_yml_suffix_path) : ''
36
-
37
- let content =
38
-
39
- prefixContent +
40
- Object
41
- .entries(model.main.srv)
42
- .filter((entry: any) => entry[1].env?.lambda?.active)
43
- .map((entry: any) => {
44
- const name = entry[0]
45
- const srv = entry[1]
46
-
47
- const lambda = srv.env.lambda
48
- const handler = lambda.handler
49
-
50
- // NOTE: gen.custom convention: allows for complete overwrite
51
- // as a get-out-of-jail
52
- if (srv.gen?.custom?.lambda?.srv_yml) {
53
- return srv.gen.custom.lambda.srv_yml
54
- }
55
-
56
- // TODO: this should be a JSON structure exported as YAML
57
- let srvyml = `${name}:
58
- handler: ${handler.path.prefix}${name}${handler.path.suffix}
59
- role: Basic${AppName}LambdaRole
60
- timeout: ${lambda.timeout}
61
- memorySize: ${lambda.memory || 1024}
62
- `
63
- const web = srv.api.web
64
-
65
- let events = ''
66
-
67
- let onEvents = srv.on
68
-
69
- if (onEvents) {
70
- Object.entries(onEvents).forEach((entry: any[]) => {
71
- // let name = entry[0]
72
- let spec = entry[1]
73
-
74
- if ('aws' === spec.provider) {
75
- spec.events.forEach((ev: any) => {
76
- if ('s3' === ev.source) {
77
- events += TM(`
78
- - s3:
79
- bucket: ${ev.bucket}
80
- event: ${ev.event}
81
- existing: true
82
- `)
83
- if (ev.rules) {
84
- events += TM(`
85
- rules:
86
- `)
87
- if (ev.rules.prefix) {
88
- events += TM(`
89
- - prefix: ${ev.rules.prefix}
90
- `)
91
- }
92
-
93
- if (ev.rules.suffix) {
94
- events += TM(`
95
- - suffix: ${ev.rules.suffix}
96
- `)
97
- }
98
- }
99
- }
100
- else if ('schedule' === ev.source) {
101
- let entries =
102
- 'string' === typeof ev.recur ? [ev.recur] : (ev.recur || [])
103
- let recur = entries.map((entry: string) => {
104
- let schedule = `
105
- - schedule:
106
- rate: ${entry}`
107
- if (ev.msg) {
108
- schedule += `
109
- input:
110
- msg: ${JSON.stringify(ev.msg)} `
111
- }
112
- return schedule
113
- })
114
-
115
- events += TM(`
116
- ${recur}
117
- `)
118
-
119
- }
120
- else if ('sqs' === ev.source) {
121
- events += TM(`
122
- - sqs:
123
- arn:
124
- Fn::GetAtt:
125
- - ${ev.qrn}
126
- - Arn
127
- batchSize: 1
128
- `)
129
-
130
- }
131
- })
132
- }
133
- })
134
- }
135
-
136
- // TODO: move to `on`
137
- if (web.active) {
138
- let prefix = web.path.prefix
139
- let suffix = web.path.suffix
140
- let area = web.path.area
141
- let method = web.method
142
- let corsflag = 'false'
143
- let corsprops = ''
144
-
145
- let methods = method.split(',')
146
- // console.log('METHODS', methods)
147
-
148
- if (web.cors.active) {
149
- corsflag = 'true'
150
- if (web.cors.props && !empty(web.cors.props)) {
151
- corsflag = ''
152
- corsprops = Object
153
- .entries(web.cors.props)
154
- .reduce(((a: any, nv: any) => (
155
- a += ` ${nv[0]}: ${nv[1]}\n`
156
- , a)), '')
157
- }
158
- }
159
-
160
- if ('v2' === web.lambda?.gateway) {
161
- for (let method of methods) {
162
- events += TM(`
163
- - httpApi:
164
- path: "${prefix}${area}${name}${suffix}"
165
- method: ${method}
166
- `)
167
- }
168
- }
169
- else {
170
- for (let method of methods) {
171
- events += TM(`
172
- - http:
173
- path: "${prefix}${area}${name}${suffix}"
174
- method: ${method}
175
- cors: ${corsflag}
176
- ${corsprops}
177
- `)
178
- }
179
- }
180
- }
181
-
182
-
183
-
184
- if ('' !== events) {
185
- srvyml += TM(`
186
- events:
187
- ${events}
188
- `)
189
- }
190
-
191
-
192
- return srvyml
193
- }).join('\n\n\n') +
194
- suffixContent
195
-
196
- Fs.writeFileSync(srv_yml_path, content)
197
- },
198
-
199
-
200
- // Only create if does not exist
201
- srv_handler: (model: any, spec: {
202
- folder: string
203
- start?: string
204
- env?: {
205
- folder: string
206
- }
207
- lang?: string
208
- }) => {
209
- let lang = spec.lang || 'js'
210
- let TS = 'ts' === lang
211
-
212
- Object
213
- .entries(model.main.srv)
214
- .filter((entry: any) => entry[1].env?.lambda)
215
- .forEach((entry: any) => {
216
- const name = entry[0]
217
- const srv = entry[1]
218
-
219
- if ('custom' === srv.env.lambda.kind) {
220
- return
221
- }
222
-
223
- let srv_handler_path = Path.join(spec.folder, name + '.' + lang)
224
-
225
- let start = spec.start || 'setup'
226
- let envFolder = spec.env?.folder || '../../../env/lambda'
227
-
228
- let handler = 'handler'
229
- let modify = ''
230
-
231
- // if (!srv.api.web.active) {
232
- // if (srv.on && 0 < Object.keys(srv.on).length) {
233
- // handler = 'eventhandler'
234
- // modify = `
235
- // event = {
236
- // ...event,
237
- // // TODO: @voxgig/system? util needed to handle this dynamically
238
- // seneca$: { msg: '${srv.on[Object.keys(srv.on)[0]].events[0].msg}' },
239
- // }
240
- // `
241
- // }
242
- // }
243
-
244
-
245
- let prepare = ''
246
- let complete = ''
247
-
248
-
249
- dive(model.main.msg.aim[name], 128).map((entry: any) => {
250
- let path = ['aim', name, ...entry[0]]
251
- let msgMeta = MsgMetaShape(entry[1])
252
- let pin = pinify(path)
253
- if (msgMeta.transport?.queue?.active) {
254
- complete += `
255
- seneca.listen({type:'sqs',pin:'${pin}'})`
256
- }
257
- })
258
-
259
-
260
- dive(model.main.srv[name].out, 128).map((entry: any) => {
261
- let path = entry[0]
262
- let msgMetaMaybe = get(model.main.msg, path)
263
- // console.log(name, path, msgMetaMaybe)
264
- if (msgMetaMaybe?.$) {
265
- let msgMeta = MsgMetaShape(msgMetaMaybe?.$)
266
- let pin = pinify(path)
267
-
268
- if (msgMeta.transport?.queue?.active) {
269
- complete += `
270
- seneca.client({type:'sqs',pin:'${pin}'})`
271
- }
272
- }
273
- })
274
-
275
- let makeGatewayHandler = false
276
- let onlist = model.main.srv[name].on || {}
277
- Object.entries(onlist).map((onitem: any) => {
278
- onitem[1].events.map((event: any) => {
279
- if ('s3' === event.source) {
280
- if (!makeGatewayHandler) {
281
- complete += `
282
-
283
- const makeGatewayHandler = seneca.export('s3-store/makeGatewayHandler')`
284
- makeGatewayHandler = true
285
- }
286
-
287
- complete += `
288
- seneca
289
- .act('sys:gateway,kind:lambda,add:hook,hook:handler', {
290
- handler: makeGatewayHandler('${event.msg}') })`
291
- }
292
- })
293
- })
294
-
295
-
296
- let content =
297
- TS ? `import { getSeneca } from '${envFolder}/${start}'`
298
- :
299
- `const getSeneca = require('${envFolder}/${start}')`
300
-
301
- content += `
302
-
303
- function complete(seneca: any) {${complete}
304
- }
305
-
306
- exports.handler = async (
307
- event${TS ? ':any' : ''},
308
- context${TS ? ':any' : ''}
309
- ) => {
310
- ${modify}
311
- let seneca = await getSeneca('${name}', complete)
312
- ${prepare}
313
- let handler = seneca.export('gateway-lambda/${handler}')
314
- let res = await handler(event, context)
315
- return res
22
+ srv_yml,
23
+ srv_handler,
24
+ resources_yml,
316
25
  }
317
- `
318
-
319
- // TODO: make this an option
320
- //if (!Fs.existsSync(srv_handler_path)) {
321
- Fs.writeFileSync(srv_handler_path, content)
322
- })
323
- },
324
-
325
-
326
- resources_yml: async (model: any, spec: {
327
- folder: string,
328
- filename: string
329
- custom: string,
330
- }) => {
331
-
332
- const core = CoreConfShape(model.main.conf.core)
333
-
334
- const appname = core.name
335
- const AppName = camelify(appname)
336
-
337
- const cloud = CloudConfShape(model.main.conf.cloud)
338
-
339
- const region = cloud.aws.region
340
- const accountid = cloud.aws.accountid
341
-
342
- let filename = spec.filename || 'resources.yml'
343
- let resources_yml_path = Path.join(spec.folder, filename)
344
-
345
- let resources_yml_prefix_path = Path.join(spec.folder, 'res.prefix.yml')
346
- let resources_yml_suffix_path = Path.join(spec.folder, 'res.suffix.yml')
347
-
348
- let prefixContent = Fs.existsSync(resources_yml_prefix_path) ?
349
- Fs.readFileSync(resources_yml_prefix_path) : ''
350
- let suffixContent = Fs.existsSync(resources_yml_suffix_path) ?
351
- Fs.readFileSync(resources_yml_suffix_path) : ''
352
-
353
- const dynamoResources: { arn: string }[] = []
354
-
355
- let content =
356
- `# START
357
- `
358
-
359
- content +=
360
- prefixContent +
361
-
362
- await res_dynamo_yml(model, { dynamoResources, region, accountid })
363
-
364
- // content +=
365
- let queueDefs = dive(model.main.msg, 128).map((entry: any) => {
366
- let path = entry[0]
367
- let msgMeta = MsgMetaShape(entry[1])
368
-
369
- let pathname = path
370
- .map((p: string) =>
371
- (p[0] + '').toUpperCase() + p.substring(1))
372
- .join('')
373
-
374
- // console.log('MQ', pathname, msgMeta)
375
-
376
- if (msgMeta.transport?.queue?.active) {
377
- // console.log('MM', path, msgMeta)
378
- let queue = msgMeta.transport.queue
379
- let name = queue.name || pathname
380
-
381
- // TODO: aontu should do this, but needs recursive child conjuncts
382
- let stage_suffix =
383
- (false === queue.stage?.active) ? '' : '-${self:provider.stage,"dev"}'
384
-
385
- let queue_suffix = (null == queue.suffix || '' == queue.suffix) ? '' :
386
- '-' + queue.suffix
387
-
388
- let resname = 'Queue' + name
389
-
390
- let queueName =
391
- (queue.prefix || '') +
392
- path.reduce((s: string, p: string, i: number) =>
393
- (s += p + (i % 2 ?
394
- (i == path.length - 1 ? '' : '-') : '_')), '') +
395
- (queue_suffix || '') +
396
- (stage_suffix || '')
397
-
398
- let queueTimeout = queue.timeout || 30
399
-
400
- return `${resname}:
401
- Type: "AWS::SQS::Queue"
402
- Properties:
403
- QueueName: '${queueName}'
404
- VisibilityTimeout: ${queueTimeout}
405
- `
406
- }
407
- return ''
408
- }).filter(n => '' !== n).join('')
409
-
410
- // console.log('queueDefs', queueDefs)
411
-
412
- content += '\n\n' + queueDefs
413
-
414
- let customLambdaPolicyStatementPath =
415
- Path.join(spec.folder, 'res.lambda.policy.statements.yml')
416
- let customLambdaPolicyStatementContent = Fs.existsSync(customLambdaPolicyStatementPath) ?
417
- Fs.readFileSync(customLambdaPolicyStatementPath) : ''
418
-
419
-
420
-
421
- content += `
422
- Basic${AppName}LambdaRole:
423
- Type: AWS::IAM::Role
424
- Properties:
425
- RoleName: Basic${AppName}LambdaRole\${self:custom.index.BasicLambdaRole,"01"}
426
- AssumeRolePolicyDocument:
427
- Version: '2012-10-17'
428
- Statement:
429
- - Effect: Allow
430
- Principal:
431
- Service:
432
- - lambda.amazonaws.com
433
- - events.amazonaws.com
434
- - ecs-tasks.amazonaws.com
435
- Action: sts:AssumeRole
436
- Policies:
437
- - PolicyName: LambdaServiceAccess
438
- PolicyDocument:
439
- Version: '2012-10-17'
440
- Statement:
441
- - Effect: Allow
442
- Action:
443
- - dynamodb:DescribeTable
444
- - dynamodb:GetItem
445
- - dynamodb:PutItem
446
- - dynamodb:UpdateItem
447
- - dynamodb:DeleteItem
448
- - dynamodb:Query
449
- - dynamodb:Scan
450
- Resource:
451
- ${dynamoResources.map(r => ' - ' + r.arn).join('\n')}
452
- ${customLambdaPolicyStatementContent}
453
- ManagedPolicyArns:
454
- - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
455
- `
456
-
457
-
458
-
459
- if (spec.custom) {
460
- content = Fs.readFileSync(spec.custom).toString() + '\n\n\n' + content
461
- }
462
-
463
- content += suffixContent
464
-
465
- content += `
466
- # END
467
- `
468
-
469
- Fs.writeFileSync(resources_yml_path, content)
470
- },
471
-
472
-
473
-
474
- }
475
-
476
-
477
-
478
-
479
-
480
-
481
- function empty(o: any) {
482
- return null == o ? true : 0 === Object.keys(o).length
483
- }
484
-
485
- // Strip inital newline
486
- function TM(str: string) {
487
- return str.replace(/^\n/, '')
488
- }
489
-
490
26
 
491
27
 
492
28
  export {
493
29
  EnvLambda,
494
30
  }
495
-
496
-
package/dist/build.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  declare const EnvLambda: {
2
2
  srv_yml: (model: any, spec: {
3
3
  folder: string;
4
- }) => void;
4
+ }) => Promise<void>;
5
5
  srv_handler: (model: any, spec: {
6
6
  folder: string;
7
7
  start?: string;
@@ -9,7 +9,7 @@ declare const EnvLambda: {
9
9
  folder: string;
10
10
  };
11
11
  lang?: string;
12
- }) => void;
12
+ }) => Promise<void>;
13
13
  resources_yml: (model: any, spec: {
14
14
  folder: string;
15
15
  filename: string;