@unrdf/serverless 26.4.2
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/README.md +305 -0
- package/package.json +81 -0
- package/src/api/api-gateway-config.mjs +390 -0
- package/src/api/index.mjs +12 -0
- package/src/cdk/index.mjs +6 -0
- package/src/cdk/unrdf-stack.mjs +363 -0
- package/src/deploy/index.mjs +10 -0
- package/src/deploy/lambda-bundler.mjs +310 -0
- package/src/index.mjs +91 -0
- package/src/storage/dynamodb-adapter.mjs +324 -0
- package/src/storage/index.mjs +6 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview UNRDF CDK Stack - Infrastructure as Code for RDF Applications
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* Defines AWS infrastructure for serverless UNRDF deployments including:
|
|
6
|
+
* - Lambda functions for RDF query execution
|
|
7
|
+
* - DynamoDB tables for RDF triple storage
|
|
8
|
+
* - API Gateway for REST endpoints
|
|
9
|
+
* - CloudFront CDN for global distribution
|
|
10
|
+
*
|
|
11
|
+
* @module serverless/cdk/unrdf-stack
|
|
12
|
+
* @version 1.0.0
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Stack, Duration, RemovalPolicy, CfnOutput } from 'aws-cdk-lib';
|
|
17
|
+
import { Runtime, Function as LambdaFunction, Code, LayerVersion } from 'aws-cdk-lib/aws-lambda';
|
|
18
|
+
import { RestApi, LambdaIntegration, Cors, EndpointType } from 'aws-cdk-lib/aws-apigateway';
|
|
19
|
+
import { Table, AttributeType, BillingMode, StreamViewType } from 'aws-cdk-lib/aws-dynamodb';
|
|
20
|
+
import {
|
|
21
|
+
Distribution,
|
|
22
|
+
OriginAccessIdentity as _OriginAccessIdentity,
|
|
23
|
+
ViewerProtocolPolicy,
|
|
24
|
+
} from 'aws-cdk-lib/aws-cloudfront';
|
|
25
|
+
import { RestApiOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
26
|
+
import { Policy as _Policy, PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam';
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration schema for UNRDF stack
|
|
31
|
+
* @typedef {Object} UNRDFStackConfig
|
|
32
|
+
* @property {string} environment - Deployment environment (dev, staging, prod)
|
|
33
|
+
* @property {number} memorySizeMb - Lambda memory allocation in MB
|
|
34
|
+
* @property {number} timeoutSeconds - Lambda timeout in seconds
|
|
35
|
+
* @property {boolean} enableCdn - Enable CloudFront CDN
|
|
36
|
+
* @property {boolean} enableStreaming - Enable DynamoDB streams
|
|
37
|
+
*/
|
|
38
|
+
const StackConfigSchema = z.object({
|
|
39
|
+
environment: z.enum(['dev', 'staging', 'prod']).default('dev'),
|
|
40
|
+
memorySizeMb: z.number().min(128).max(10240).default(1024),
|
|
41
|
+
timeoutSeconds: z.number().min(3).max(900).default(30),
|
|
42
|
+
enableCdn: z.boolean().default(true),
|
|
43
|
+
enableStreaming: z.boolean().default(false),
|
|
44
|
+
tableName: z.string().optional(),
|
|
45
|
+
apiName: z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* UNRDF Serverless Stack
|
|
50
|
+
*
|
|
51
|
+
* @class UNRDFStack
|
|
52
|
+
* @extends {Stack}
|
|
53
|
+
*
|
|
54
|
+
* @description
|
|
55
|
+
* Creates a complete serverless infrastructure for UNRDF applications with:
|
|
56
|
+
* - Auto-scaling Lambda functions
|
|
57
|
+
* - DynamoDB for persistent RDF storage
|
|
58
|
+
* - API Gateway with CORS support
|
|
59
|
+
* - Optional CloudFront CDN
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```javascript
|
|
63
|
+
* import { App } from 'aws-cdk-lib';
|
|
64
|
+
* import { UNRDFStack } from '@unrdf/serverless/cdk';
|
|
65
|
+
*
|
|
66
|
+
* const app = new App();
|
|
67
|
+
* new UNRDFStack(app, 'MyUNRDFApp', {
|
|
68
|
+
* environment: 'prod',
|
|
69
|
+
* memorySizeMb: 2048,
|
|
70
|
+
* enableCdn: true
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class UNRDFStack extends Stack {
|
|
75
|
+
/**
|
|
76
|
+
* DynamoDB table for RDF triples
|
|
77
|
+
* @type {Table}
|
|
78
|
+
*/
|
|
79
|
+
triplesTable;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lambda function for query execution
|
|
83
|
+
* @type {LambdaFunction}
|
|
84
|
+
*/
|
|
85
|
+
queryFunction;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* API Gateway REST API
|
|
89
|
+
* @type {RestApi}
|
|
90
|
+
*/
|
|
91
|
+
api;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* CloudFront distribution (optional)
|
|
95
|
+
* @type {Distribution|null}
|
|
96
|
+
*/
|
|
97
|
+
distribution = null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create UNRDF Stack
|
|
101
|
+
*
|
|
102
|
+
* @param {import('constructs').Construct} scope - CDK scope
|
|
103
|
+
* @param {string} id - Stack ID
|
|
104
|
+
* @param {Object} props - Stack properties
|
|
105
|
+
* @param {Object} [props.config] - UNRDF configuration
|
|
106
|
+
*/
|
|
107
|
+
constructor(scope, id, props = {}) {
|
|
108
|
+
super(scope, id, props);
|
|
109
|
+
|
|
110
|
+
// Validate configuration
|
|
111
|
+
const config = StackConfigSchema.parse(props.config || {});
|
|
112
|
+
|
|
113
|
+
// Create DynamoDB table for RDF triples
|
|
114
|
+
this.triplesTable = this.createTriplesTable(config);
|
|
115
|
+
|
|
116
|
+
// Create Lambda layer with dependencies
|
|
117
|
+
const layer = this.createDependencyLayer();
|
|
118
|
+
|
|
119
|
+
// Create Lambda functions
|
|
120
|
+
this.queryFunction = this.createQueryFunction(config, layer);
|
|
121
|
+
this.createIngestFunction(config, layer);
|
|
122
|
+
|
|
123
|
+
// Grant DynamoDB access to Lambda
|
|
124
|
+
this.triplesTable.grantReadWriteData(this.queryFunction);
|
|
125
|
+
|
|
126
|
+
// Create API Gateway
|
|
127
|
+
this.api = this.createApiGateway(config);
|
|
128
|
+
|
|
129
|
+
// Create CloudFront CDN if enabled
|
|
130
|
+
if (config.enableCdn) {
|
|
131
|
+
this.distribution = this.createCdnDistribution();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Output deployment information
|
|
135
|
+
this.createOutputs(config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create DynamoDB table for RDF triple storage
|
|
140
|
+
*
|
|
141
|
+
* @private
|
|
142
|
+
* @param {Object} config - Stack configuration
|
|
143
|
+
* @returns {Table} DynamoDB table
|
|
144
|
+
*/
|
|
145
|
+
createTriplesTable(config) {
|
|
146
|
+
const table = new Table(this, 'TriplesTable', {
|
|
147
|
+
tableName: config.tableName || `unrdf-triples-${config.environment}`,
|
|
148
|
+
partitionKey: { name: 'subject', type: AttributeType.STRING },
|
|
149
|
+
sortKey: { name: 'predicate_object', type: AttributeType.STRING },
|
|
150
|
+
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
151
|
+
stream: config.enableStreaming ? StreamViewType.NEW_AND_OLD_IMAGES : undefined,
|
|
152
|
+
removalPolicy: config.environment === 'prod' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
|
|
153
|
+
pointInTimeRecovery: config.environment === 'prod',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Add GSI for predicate queries
|
|
157
|
+
table.addGlobalSecondaryIndex({
|
|
158
|
+
indexName: 'predicate-index',
|
|
159
|
+
partitionKey: { name: 'predicate', type: AttributeType.STRING },
|
|
160
|
+
sortKey: { name: 'subject_object', type: AttributeType.STRING },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Add GSI for object queries
|
|
164
|
+
table.addGlobalSecondaryIndex({
|
|
165
|
+
indexName: 'object-index',
|
|
166
|
+
partitionKey: { name: 'object', type: AttributeType.STRING },
|
|
167
|
+
sortKey: { name: 'subject_predicate', type: AttributeType.STRING },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return table;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create Lambda layer with UNRDF dependencies
|
|
175
|
+
*
|
|
176
|
+
* @private
|
|
177
|
+
* @returns {LayerVersion} Lambda layer
|
|
178
|
+
*/
|
|
179
|
+
createDependencyLayer() {
|
|
180
|
+
return new LayerVersion(this, 'UNRDFLayer', {
|
|
181
|
+
code: Code.fromAsset('dist/layer'),
|
|
182
|
+
compatibleRuntimes: [Runtime.NODEJS_20_X],
|
|
183
|
+
description: 'UNRDF core dependencies and utilities',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create Lambda function for SPARQL query execution
|
|
189
|
+
*
|
|
190
|
+
* @private
|
|
191
|
+
* @param {Object} config - Stack configuration
|
|
192
|
+
* @param {LayerVersion} layer - Dependency layer
|
|
193
|
+
* @returns {LambdaFunction} Query function
|
|
194
|
+
*/
|
|
195
|
+
createQueryFunction(config, layer) {
|
|
196
|
+
const fn = new LambdaFunction(this, 'QueryFunction', {
|
|
197
|
+
functionName: `unrdf-query-${config.environment}`,
|
|
198
|
+
runtime: Runtime.NODEJS_20_X,
|
|
199
|
+
handler: 'index.handler',
|
|
200
|
+
code: Code.fromAsset('dist/lambda/query'),
|
|
201
|
+
layers: [layer],
|
|
202
|
+
memorySize: config.memorySizeMb,
|
|
203
|
+
timeout: Duration.seconds(config.timeoutSeconds),
|
|
204
|
+
environment: {
|
|
205
|
+
TRIPLES_TABLE: this.triplesTable.tableName,
|
|
206
|
+
ENVIRONMENT: config.environment,
|
|
207
|
+
NODE_ENV: 'production',
|
|
208
|
+
},
|
|
209
|
+
reservedConcurrentExecutions: config.environment === 'prod' ? 100 : 10,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Add X-Ray tracing
|
|
213
|
+
fn.addToRolePolicy(
|
|
214
|
+
new PolicyStatement({
|
|
215
|
+
effect: Effect.ALLOW,
|
|
216
|
+
actions: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
|
|
217
|
+
resources: ['*'],
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return fn;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create Lambda function for RDF data ingestion
|
|
226
|
+
*
|
|
227
|
+
* @private
|
|
228
|
+
* @param {Object} config - Stack configuration
|
|
229
|
+
* @param {LayerVersion} layer - Dependency layer
|
|
230
|
+
* @returns {LambdaFunction} Ingest function
|
|
231
|
+
*/
|
|
232
|
+
createIngestFunction(config, layer) {
|
|
233
|
+
const fn = new LambdaFunction(this, 'IngestFunction', {
|
|
234
|
+
functionName: `unrdf-ingest-${config.environment}`,
|
|
235
|
+
runtime: Runtime.NODEJS_20_X,
|
|
236
|
+
handler: 'index.handler',
|
|
237
|
+
code: Code.fromAsset('dist/lambda/ingest'),
|
|
238
|
+
layers: [layer],
|
|
239
|
+
memorySize: config.memorySizeMb * 2, // Double memory for batch operations
|
|
240
|
+
timeout: Duration.seconds(Math.min(config.timeoutSeconds * 2, 900)),
|
|
241
|
+
environment: {
|
|
242
|
+
TRIPLES_TABLE: this.triplesTable.tableName,
|
|
243
|
+
ENVIRONMENT: config.environment,
|
|
244
|
+
BATCH_SIZE: '100',
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
this.triplesTable.grantReadWriteData(fn);
|
|
249
|
+
|
|
250
|
+
return fn;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create API Gateway REST API
|
|
255
|
+
*
|
|
256
|
+
* @private
|
|
257
|
+
* @param {Object} config - Stack configuration
|
|
258
|
+
* @returns {RestApi} API Gateway
|
|
259
|
+
*/
|
|
260
|
+
createApiGateway(config) {
|
|
261
|
+
const api = new RestApi(this, 'UNRDFApi', {
|
|
262
|
+
restApiName: config.apiName || `unrdf-api-${config.environment}`,
|
|
263
|
+
description: 'UNRDF Serverless API for RDF operations',
|
|
264
|
+
endpointTypes: [EndpointType.REGIONAL],
|
|
265
|
+
defaultCorsPreflightOptions: {
|
|
266
|
+
allowOrigins: Cors.ALL_ORIGINS,
|
|
267
|
+
allowMethods: Cors.ALL_METHODS,
|
|
268
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
269
|
+
},
|
|
270
|
+
deployOptions: {
|
|
271
|
+
stageName: config.environment,
|
|
272
|
+
tracingEnabled: true,
|
|
273
|
+
metricsEnabled: true,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Add /query endpoint
|
|
278
|
+
const queryResource = api.root.addResource('query');
|
|
279
|
+
queryResource.addMethod('POST', new LambdaIntegration(this.queryFunction), {
|
|
280
|
+
apiKeyRequired: config.environment === 'prod',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Add /health endpoint
|
|
284
|
+
const healthResource = api.root.addResource('health');
|
|
285
|
+
healthResource.addMethod('GET', new LambdaIntegration(this.queryFunction));
|
|
286
|
+
|
|
287
|
+
return api;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create CloudFront CDN distribution
|
|
292
|
+
*
|
|
293
|
+
* @private
|
|
294
|
+
* @returns {Distribution} CloudFront distribution
|
|
295
|
+
*/
|
|
296
|
+
createCdnDistribution() {
|
|
297
|
+
const distribution = new Distribution(this, 'UNRDFDistribution', {
|
|
298
|
+
defaultBehavior: {
|
|
299
|
+
origin: new RestApiOrigin(this.api),
|
|
300
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
301
|
+
compress: true,
|
|
302
|
+
},
|
|
303
|
+
comment: 'UNRDF API CDN distribution',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return distribution;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create CloudFormation outputs
|
|
311
|
+
*
|
|
312
|
+
* @private
|
|
313
|
+
* @param {Object} config - Stack configuration
|
|
314
|
+
*/
|
|
315
|
+
createOutputs(config) {
|
|
316
|
+
new CfnOutput(this, 'ApiEndpoint', {
|
|
317
|
+
value: this.api.url,
|
|
318
|
+
description: 'API Gateway endpoint URL',
|
|
319
|
+
exportName: `unrdf-api-endpoint-${config.environment}`,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
new CfnOutput(this, 'TriplesTableName', {
|
|
323
|
+
value: this.triplesTable.tableName,
|
|
324
|
+
description: 'DynamoDB triples table name',
|
|
325
|
+
exportName: `unrdf-triples-table-${config.environment}`,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
new CfnOutput(this, 'QueryFunctionArn', {
|
|
329
|
+
value: this.queryFunction.functionArn,
|
|
330
|
+
description: 'Query Lambda function ARN',
|
|
331
|
+
exportName: `unrdf-query-function-${config.environment}`,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (this.distribution) {
|
|
335
|
+
new CfnOutput(this, 'DistributionDomain', {
|
|
336
|
+
value: this.distribution.distributionDomainName,
|
|
337
|
+
description: 'CloudFront distribution domain',
|
|
338
|
+
exportName: `unrdf-cdn-domain-${config.environment}`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create UNRDF stack from configuration
|
|
346
|
+
*
|
|
347
|
+
* @param {import('constructs').Construct} scope - CDK app
|
|
348
|
+
* @param {string} id - Stack identifier
|
|
349
|
+
* @param {Object} config - Stack configuration
|
|
350
|
+
* @returns {UNRDFStack} Initialized stack
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```javascript
|
|
354
|
+
* const stack = createUNRDFStack(app, 'Production', {
|
|
355
|
+
* environment: 'prod',
|
|
356
|
+
* memorySizeMb: 2048,
|
|
357
|
+
* enableCdn: true
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
export function createUNRDFStack(scope, id, config) {
|
|
362
|
+
return new UNRDFStack(scope, id, { config });
|
|
363
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lambda Function Bundler - esbuild integration for UNRDF
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* Bundles UNRDF applications into optimized Lambda deployment packages using esbuild.
|
|
6
|
+
* Handles dependency resolution, minification, and tree-shaking for minimal cold starts.
|
|
7
|
+
*
|
|
8
|
+
* @module serverless/deploy/lambda-bundler
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { build } from 'esbuild';
|
|
14
|
+
import { createWriteStream, promises as fs } from 'node:fs';
|
|
15
|
+
import { join, dirname as _dirname } from 'node:path';
|
|
16
|
+
import { createGzip } from 'node:zlib';
|
|
17
|
+
import { pipeline } from 'node:stream/promises';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Bundler configuration schema
|
|
22
|
+
* @typedef {Object} BundlerConfig
|
|
23
|
+
* @property {string} entryPoint - Entry point file path
|
|
24
|
+
* @property {string} outDir - Output directory
|
|
25
|
+
* @property {boolean} minify - Enable minification
|
|
26
|
+
* @property {boolean} sourcemap - Generate source maps
|
|
27
|
+
* @property {string[]} external - External dependencies
|
|
28
|
+
* @property {Object} define - Environment variable definitions
|
|
29
|
+
*/
|
|
30
|
+
const BundlerConfigSchema = z.object({
|
|
31
|
+
entryPoint: z.string(),
|
|
32
|
+
outDir: z.string(),
|
|
33
|
+
minify: z.boolean().default(true),
|
|
34
|
+
sourcemap: z.boolean().default(false),
|
|
35
|
+
external: z.array(z.string()).default(['@aws-sdk/*']),
|
|
36
|
+
define: z.record(z.string()).default({}),
|
|
37
|
+
platform: z.enum(['node', 'browser']).default('node'),
|
|
38
|
+
target: z.string().default('node20'),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bundle metadata
|
|
43
|
+
* @typedef {Object} BundleMetadata
|
|
44
|
+
* @property {string} outputPath - Bundled file path
|
|
45
|
+
* @property {number} sizeBytes - Bundle size in bytes
|
|
46
|
+
* @property {number} gzipSizeBytes - Gzipped size in bytes
|
|
47
|
+
* @property {string[]} dependencies - Included dependencies
|
|
48
|
+
* @property {number} buildTimeMs - Build duration in milliseconds
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lambda Function Bundler
|
|
53
|
+
*
|
|
54
|
+
* @class LambdaBundler
|
|
55
|
+
*
|
|
56
|
+
* @description
|
|
57
|
+
* High-performance bundler for Lambda functions with:
|
|
58
|
+
* - Tree-shaking for minimal bundle size
|
|
59
|
+
* - Automatic external dependency detection
|
|
60
|
+
* - Source map generation for debugging
|
|
61
|
+
* - Gzip compression for deployment
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```javascript
|
|
65
|
+
* const bundler = new LambdaBundler({
|
|
66
|
+
* entryPoint: './src/handler.mjs',
|
|
67
|
+
* outDir: './dist/lambda',
|
|
68
|
+
* minify: true
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* const metadata = await bundler.bundle();
|
|
72
|
+
* console.log(`Bundle size: ${metadata.sizeBytes} bytes`);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class LambdaBundler {
|
|
76
|
+
/**
|
|
77
|
+
* Bundler configuration
|
|
78
|
+
* @type {Object}
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
#config;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create Lambda bundler
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} config - Bundler configuration
|
|
87
|
+
*/
|
|
88
|
+
constructor(config) {
|
|
89
|
+
this.#config = BundlerConfigSchema.parse(config);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bundle Lambda function
|
|
94
|
+
*
|
|
95
|
+
* @returns {Promise<BundleMetadata>} Bundle metadata
|
|
96
|
+
*
|
|
97
|
+
* @throws {Error} If bundling fails
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```javascript
|
|
101
|
+
* const metadata = await bundler.bundle();
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
async bundle() {
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Ensure output directory exists
|
|
109
|
+
await fs.mkdir(this.#config.outDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
// Build with esbuild
|
|
112
|
+
const result = await build({
|
|
113
|
+
entryPoints: [this.#config.entryPoint],
|
|
114
|
+
bundle: true,
|
|
115
|
+
platform: this.#config.platform,
|
|
116
|
+
target: this.#config.target,
|
|
117
|
+
format: 'esm',
|
|
118
|
+
outdir: this.#config.outDir,
|
|
119
|
+
minify: this.#config.minify,
|
|
120
|
+
sourcemap: this.#config.sourcemap,
|
|
121
|
+
external: this.#config.external,
|
|
122
|
+
define: this.#config.define,
|
|
123
|
+
treeShaking: true,
|
|
124
|
+
metafile: true,
|
|
125
|
+
logLevel: 'info',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const outputPath = join(this.#config.outDir, 'index.js');
|
|
129
|
+
const stats = await fs.stat(outputPath);
|
|
130
|
+
|
|
131
|
+
// Create gzipped version
|
|
132
|
+
const gzipPath = `${outputPath}.gz`;
|
|
133
|
+
await this.#gzipFile(outputPath, gzipPath);
|
|
134
|
+
const gzipStats = await fs.stat(gzipPath);
|
|
135
|
+
|
|
136
|
+
// Extract dependencies from metafile
|
|
137
|
+
const dependencies = this.#extractDependencies(result.metafile);
|
|
138
|
+
|
|
139
|
+
const buildTimeMs = Date.now() - startTime;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
outputPath,
|
|
143
|
+
sizeBytes: stats.size,
|
|
144
|
+
gzipSizeBytes: gzipStats.size,
|
|
145
|
+
dependencies,
|
|
146
|
+
buildTimeMs,
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new Error(`Bundle failed: ${error.message}`, { cause: error });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Bundle multiple Lambda functions
|
|
155
|
+
*
|
|
156
|
+
* @param {Object[]} configs - Array of bundler configurations
|
|
157
|
+
* @returns {Promise<BundleMetadata[]>} Array of bundle metadata
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```javascript
|
|
161
|
+
* const results = await LambdaBundler.bundleAll([
|
|
162
|
+
* { entryPoint: './src/query.mjs', outDir: './dist/query' },
|
|
163
|
+
* { entryPoint: './src/ingest.mjs', outDir: './dist/ingest' }
|
|
164
|
+
* ]);
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
static async bundleAll(configs) {
|
|
168
|
+
const bundlers = configs.map(config => new LambdaBundler(config));
|
|
169
|
+
return Promise.all(bundlers.map(bundler => bundler.bundle()));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gzip a file
|
|
174
|
+
*
|
|
175
|
+
* @private
|
|
176
|
+
* @param {string} inputPath - Input file path
|
|
177
|
+
* @param {string} outputPath - Output gzip path
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
async #gzipFile(inputPath, outputPath) {
|
|
181
|
+
const input = (await import('node:fs')).createReadStream(inputPath);
|
|
182
|
+
const output = createWriteStream(outputPath);
|
|
183
|
+
const gzip = createGzip({ level: 9 });
|
|
184
|
+
|
|
185
|
+
await pipeline(input, gzip, output);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extract dependencies from esbuild metafile
|
|
190
|
+
*
|
|
191
|
+
* @private
|
|
192
|
+
* @param {Object} metafile - esbuild metafile
|
|
193
|
+
* @returns {string[]} List of dependencies
|
|
194
|
+
*/
|
|
195
|
+
#extractDependencies(metafile) {
|
|
196
|
+
const deps = new Set();
|
|
197
|
+
|
|
198
|
+
for (const input of Object.keys(metafile.inputs || {})) {
|
|
199
|
+
if (input.includes('node_modules')) {
|
|
200
|
+
const match = input.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
|
|
201
|
+
if (match) {
|
|
202
|
+
deps.add(match[1]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Array.from(deps).sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Analyze bundle size and composition
|
|
212
|
+
*
|
|
213
|
+
* @param {string} metafilePath - Path to esbuild metafile
|
|
214
|
+
* @returns {Promise<Object>} Bundle analysis
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```javascript
|
|
218
|
+
* const analysis = await LambdaBundler.analyzeBundleSize('./dist/metafile.json');
|
|
219
|
+
* console.log('Largest dependencies:', analysis.largestDeps);
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
static async analyzeBundleSize(metafilePath) {
|
|
223
|
+
const content = await fs.readFile(metafilePath, 'utf-8');
|
|
224
|
+
const metafile = JSON.parse(content);
|
|
225
|
+
|
|
226
|
+
const inputs = metafile.inputs || {};
|
|
227
|
+
const sizeByModule = {};
|
|
228
|
+
|
|
229
|
+
for (const [path, data] of Object.entries(inputs)) {
|
|
230
|
+
const bytes = data.bytes || 0;
|
|
231
|
+
const moduleName = path.includes('node_modules')
|
|
232
|
+
? path.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/)?.[1] || 'unknown'
|
|
233
|
+
: 'application';
|
|
234
|
+
|
|
235
|
+
sizeByModule[moduleName] = (sizeByModule[moduleName] || 0) + bytes;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sorted = Object.entries(sizeByModule)
|
|
239
|
+
.sort(([, a], [, b]) => b - a)
|
|
240
|
+
.slice(0, 10);
|
|
241
|
+
|
|
242
|
+
const totalSize = Object.values(sizeByModule).reduce((sum, size) => sum + size, 0);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
totalSizeBytes: totalSize,
|
|
246
|
+
largestDeps: sorted.map(([name, bytes]) => ({
|
|
247
|
+
name,
|
|
248
|
+
bytes,
|
|
249
|
+
percentage: ((bytes / totalSize) * 100).toFixed(2),
|
|
250
|
+
})),
|
|
251
|
+
moduleCount: Object.keys(sizeByModule).length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create default UNRDF Lambda bundler configuration
|
|
258
|
+
*
|
|
259
|
+
* @param {string} functionName - Lambda function name (query, ingest, etc.)
|
|
260
|
+
* @param {Object} options - Additional options
|
|
261
|
+
* @returns {Object} Bundler configuration
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```javascript
|
|
265
|
+
* const config = createDefaultBundlerConfig('query', { minify: true });
|
|
266
|
+
* const bundler = new LambdaBundler(config);
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
export function createDefaultBundlerConfig(functionName, options = {}) {
|
|
270
|
+
return {
|
|
271
|
+
entryPoint: `./src/lambda/${functionName}/index.mjs`,
|
|
272
|
+
outDir: `./dist/lambda/${functionName}`,
|
|
273
|
+
minify: true,
|
|
274
|
+
sourcemap: false,
|
|
275
|
+
external: ['@aws-sdk/*'],
|
|
276
|
+
define: {
|
|
277
|
+
'process.env.NODE_ENV': '"production"',
|
|
278
|
+
'process.env.FUNCTION_NAME': `"${functionName}"`,
|
|
279
|
+
},
|
|
280
|
+
...options,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Bundle all UNRDF Lambda functions
|
|
286
|
+
*
|
|
287
|
+
* @param {Object} options - Bundle options
|
|
288
|
+
* @returns {Promise<Map<string, BundleMetadata>>} Map of function name to metadata
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```javascript
|
|
292
|
+
* const results = await bundleUNRDFFunctions({ minify: true });
|
|
293
|
+
* for (const [name, metadata] of results) {
|
|
294
|
+
* console.log(`${name}: ${metadata.sizeBytes} bytes`);
|
|
295
|
+
* }
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export async function bundleUNRDFFunctions(options = {}) {
|
|
299
|
+
const functions = ['query', 'ingest'];
|
|
300
|
+
const results = new Map();
|
|
301
|
+
|
|
302
|
+
for (const fn of functions) {
|
|
303
|
+
const config = createDefaultBundlerConfig(fn, options);
|
|
304
|
+
const bundler = new LambdaBundler(config);
|
|
305
|
+
const metadata = await bundler.bundle();
|
|
306
|
+
results.set(fn, metadata);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return results;
|
|
310
|
+
}
|