@voxgig/build 3.1.0 → 4.1.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,63 @@
1
- /* Copyright © 2022 Voxgig Ltd, MIT License. */
2
-
3
- import Fs from 'fs'
4
- import Path from 'path'
5
-
6
- import { dive, get, pinify, camelify } from '@voxgig/util'
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'
19
+
20
+ import {
21
+ generate, empty, TM,
22
+ loadFragment, renderFragment, listFragments, PKG_TM,
23
+ } from './env/lambda/generate'
7
24
 
8
25
  import { MsgMetaShape } from './shape/msg'
9
26
  import { CoreConfShape, CloudConfShape } from './shape/conf'
10
-
11
27
  import { res_dynamo_yml } from './yml/res_dynamo_yml'
12
28
 
13
29
 
14
- // console.log('BUILD 1')
15
-
16
30
  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
31
+ srv_yml,
32
+ srv_handler,
33
+ resources_yml,
316
34
  }
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
35
 
473
36
 
37
+ // Fragment tooling (used by voxgig-system template list/eject/diff, and
38
+ // available to project code).
39
+ const Fragments = {
40
+ load: loadFragment,
41
+ render: renderFragment,
42
+ list: listFragments,
43
+ folder: PKG_TM,
474
44
  }
475
45
 
476
46
 
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
-
491
-
492
47
  export {
493
48
  EnvLambda,
49
+ Fragments,
50
+
51
+ // Building blocks for ejected project templates (src/gen/<name>.ts
52
+ // copies import these from '@voxgig/build' instead of package
53
+ // internals - see voxgig-system template eject --code).
54
+ generate,
55
+ empty,
56
+ TM,
57
+ loadFragment,
58
+ renderFragment,
59
+ MsgMetaShape,
60
+ CoreConfShape,
61
+ CloudConfShape,
62
+ res_dynamo_yml,
494
63
  }
495
-
496
-
package/dist/build.d.ts CHANGED
@@ -1,7 +1,12 @@
1
+ import { generate, empty, TM, loadFragment, renderFragment, listFragments } from './env/lambda/generate';
2
+ import { MsgMetaShape } from './shape/msg';
3
+ import { CoreConfShape, CloudConfShape } from './shape/conf';
4
+ import { res_dynamo_yml } from './yml/res_dynamo_yml';
1
5
  declare const EnvLambda: {
2
6
  srv_yml: (model: any, spec: {
3
7
  folder: string;
4
- }) => void;
8
+ tm?: string;
9
+ }) => Promise<void>;
5
10
  srv_handler: (model: any, spec: {
6
11
  folder: string;
7
12
  start?: string;
@@ -9,11 +14,19 @@ declare const EnvLambda: {
9
14
  folder: string;
10
15
  };
11
16
  lang?: string;
12
- }) => void;
17
+ tm?: string;
18
+ }) => Promise<void>;
13
19
  resources_yml: (model: any, spec: {
14
20
  folder: string;
15
21
  filename: string;
16
22
  custom: string;
23
+ tm?: string;
17
24
  }) => Promise<void>;
18
25
  };
19
- export { EnvLambda, };
26
+ declare const Fragments: {
27
+ load: typeof loadFragment;
28
+ render: typeof renderFragment;
29
+ list: typeof listFragments;
30
+ folder: string;
31
+ };
32
+ export { EnvLambda, Fragments, generate, empty, TM, loadFragment, renderFragment, MsgMetaShape, CoreConfShape, CloudConfShape, res_dynamo_yml, };