UNPKG

14.2 kBJavaScriptView Raw
1"use strict";
2const _ = require("lodash");
3const BbPromise = require("bluebird");
4const AWS = require("aws-sdk");
5const dynamodbLocal = require("dynamodb-localhost");
6const seeder = require("./src/seeder");
7const path = require('path');
8
9class ServerlessDynamodbLocal {
10 constructor(serverless, options) {
11 this.serverless = serverless;
12 this.service = serverless.service;
13 this.serverlessLog = serverless.cli.log.bind(serverless.cli);
14 this.config = this.service.custom && this.service.custom.dynamodb || {};
15 this.options = _.merge({
16 localPath: path.join(serverless.config.servicePath, '.dynamodb')
17 },
18 options
19 );
20 this.provider = "aws";
21 this.commands = {
22 dynamodb: {
23 commands: {
24 migrate: {
25 lifecycleEvents: ["migrateHandler"],
26 usage: "Creates local DynamoDB tables from the current Serverless configuration"
27 },
28 seed: {
29 lifecycleEvents: ["seedHandler"],
30 usage: "Seeds local DynamoDB tables with data",
31 options: {
32 online: {
33 shortcut: "o",
34 usage: "Will connect to the tables online to do an online seed run"
35 }
36 }
37 },
38 start: {
39 lifecycleEvents: ["startHandler"],
40 usage: "Starts local DynamoDB",
41 options: {
42 port: {
43 shortcut: "p",
44 usage: "The port number that DynamoDB will use to communicate with your application. If you do not specify this option, the default port is 8000"
45 },
46 cors: {
47 shortcut: "c",
48 usage: "Enable CORS support (cross-origin resource sharing) for JavaScript. You must provide a comma-separated \"allow\" list of specific domains. The default setting for -cors is an asterisk (*), which allows public access."
49 },
50 inMemory: {
51 shortcut: "i",
52 usage: "DynamoDB; will run in memory, instead of using a database file. When you stop DynamoDB;, none of the data will be saved. Note that you cannot specify both -dbPath and -inMemory at once."
53 },
54 dbPath: {
55 shortcut: "d",
56 usage: "The directory where DynamoDB will write its database file. If you do not specify this option, the file will be written to the current directory. Note that you cannot specify both -dbPath and -inMemory at once. For the path, current working directory is <projectroot>/node_modules/serverless-dynamodb-local/dynamob. For example to create <projectroot>/node_modules/serverless-dynamodb-local/dynamob/<mypath> you should specify -d <mypath>/ or --dbPath <mypath>/ with a forwardslash at the end."
57 },
58 sharedDb: {
59 shortcut: "h",
60 usage: "DynamoDB will use a single database file, instead of using separate files for each credential and region. If you specify -sharedDb, all DynamoDB clients will interact with the same set of tables regardless of their region and credential configuration."
61 },
62 delayTransientStatuses: {
63 shortcut: "t",
64 usage: "Causes DynamoDB to introduce delays for certain operations. DynamoDB can perform some tasks almost instantaneously, such as create/update/delete operations on tables and indexes; however, the actual DynamoDB service requires more time for these tasks. Setting this parameter helps DynamoDB simulate the behavior of the Amazon DynamoDB web service more closely. (Currently, this parameter introduces delays only for global secondary indexes that are in either CREATING or DELETING status."
65 },
66 optimizeDbBeforeStartup: {
67 shortcut: "o",
68 usage: "Optimizes the underlying database tables before starting up DynamoDB on your computer. You must also specify -dbPath when you use this parameter."
69 },
70 migrate: {
71 shortcut: "m",
72 usage: "After starting dynamodb local, create DynamoDB tables from the current serverless configuration."
73 },
74 seed: {
75 shortcut: "s",
76 usage: "After starting and migrating dynamodb local, injects seed data into your tables. The --seed option determines which data categories to onload.",
77 },
78 convertEmptyValues: {
79 shortcut: "e",
80 usage: "Set to true if you would like the document client to convert empty values (0-length strings, binary buffers, and sets) to be converted to NULL types when persisting to DynamoDB.",
81 }
82 }
83 },
84 noStart: {
85 shortcut: "n",
86 default: false,
87 usage: "Do not start DynamoDB local (in case it is already running)",
88 },
89 remove: {
90 lifecycleEvents: ["removeHandler"],
91 usage: "Removes local DynamoDB"
92 },
93 install: {
94 usage: "Installs local DynamoDB",
95 lifecycleEvents: ["installHandler"],
96 options: {
97 localPath: {
98 shortcut: "x",
99 usage: "Local dynamodb install path"
100 }
101 }
102
103 }
104 }
105 }
106 };
107
108 const stage = this.options.stage || this.service.provider.stage;
109 if (this.config.stages && !this.config.stages.includes(stage)) {
110 // don't do anything for this stage
111 this.hooks = {};
112 return;
113 }
114
115 this.hooks = {
116 "dynamodb:migrate:migrateHandler": this.migrateHandler.bind(this),
117 "dynamodb:seed:seedHandler": this.seedHandler.bind(this),
118 "dynamodb:remove:removeHandler": this.removeHandler.bind(this),
119 "dynamodb:install:installHandler": this.installHandler.bind(this),
120 "dynamodb:start:startHandler": this.startHandler.bind(this),
121 "before:offline:start:init": this.startHandler.bind(this),
122 "before:offline:start:end": this.endHandler.bind(this),
123 };
124 }
125
126 get port() {
127 const config = this.service.custom && this.service.custom.dynamodb || {};
128 const port = _.get(config, "start.port", 8000);
129 return port;
130 }
131
132 get host() {
133 const config = this.service.custom && this.service.custom.dynamodb || {};
134 const host = _.get(config, "start.host", "localhost");
135 return host;
136 }
137
138 dynamodbOptions(options) {
139 let dynamoOptions = {};
140
141 if(options && options.online){
142 this.serverlessLog("Connecting to online tables...");
143 if (!options.region) {
144 throw new Error("please specify the region");
145 }
146 dynamoOptions = {
147 region: options.region,
148 convertEmptyValues: options && options.convertEmptyValues ? options.convertEmptyValues : false,
149 };
150 } else {
151 dynamoOptions = {
152 endpoint: `http://${this.host}:${this.port}`,
153 region: "localhost",
154 accessKeyId: "MOCK_ACCESS_KEY_ID",
155 secretAccessKey: "MOCK_SECRET_ACCESS_KEY",
156 convertEmptyValues: options && options.convertEmptyValues ? options.convertEmptyValues : false,
157 };
158 }
159
160 return {
161 raw: new AWS.DynamoDB(dynamoOptions),
162 doc: new AWS.DynamoDB.DocumentClient(dynamoOptions)
163 };
164 }
165
166 migrateHandler() {
167 const dynamodb = this.dynamodbOptions();
168 const tables = this.tables;
169 return BbPromise.each(tables, (table) => this.createTable(dynamodb, table));
170 }
171
172 seedHandler() {
173 const options = this.options;
174 const dynamodb = this.dynamodbOptions(options);
175
176 return BbPromise.each(this.seedSources, (source) => {
177 if (!source.table) {
178 throw new Error("seeding source \"table\" property not defined");
179 }
180 const seedPromise = seeder.locateSeeds(source.sources || [])
181 .then((seeds) => seeder.writeSeeds(dynamodb.doc.batchWrite.bind(dynamodb.doc), source.table, seeds));
182 const rawSeedPromise = seeder.locateSeeds(source.rawsources || [])
183 .then((seeds) => seeder.writeSeeds(dynamodb.raw.batchWriteItem.bind(dynamodb.raw), source.table, seeds));
184 return BbPromise.all([seedPromise, rawSeedPromise]);
185 });
186 }
187
188 removeHandler() {
189 return new BbPromise((resolve) => dynamodbLocal.remove(resolve));
190 }
191
192 installHandler() {
193 const options = this.options;
194 return new BbPromise((resolve) => dynamodbLocal.install(resolve, options.localPath));
195 }
196
197 startHandler() {
198 const config = this.service.custom && this.service.custom.dynamodb || {};
199 const options = _.merge({
200 sharedDb: this.options.sharedDb || true,
201 install_path: this.options.localPath
202 },
203 config && config.start,
204 this.options
205 );
206
207 // otherwise endHandler will be mis-informed
208 this.options = options;
209 if (!options.noStart) {
210 dynamodbLocal.start(options);
211 }
212 return BbPromise.resolve()
213 .then(() => options.migrate && this.migrateHandler())
214 .then(() => options.seed && this.seedHandler());
215 }
216
217 endHandler() {
218 if (!this.options.noStart) {
219 this.serverlessLog("DynamoDB - stopping local database");
220 dynamodbLocal.stop(this.port);
221 }
222 }
223
224 getDefaultStack() {
225 return _.get(this.service, "resources");
226 }
227
228 getAdditionalStacks() {
229 return _.values(_.get(this.service, "custom.additionalStacks", {}));
230 }
231
232 hasAdditionalStacksPlugin() {
233 return _.get(this.service, "plugins", []).includes("serverless-plugin-additional-stacks");
234 }
235
236 getTableDefinitionsFromStack(stack) {
237 const resources = _.get(stack, "Resources", []);
238 return Object.keys(resources).map((key) => {
239 if (resources[key].Type === "AWS::DynamoDB::Table") {
240 return resources[key].Properties;
241 }
242 }).filter((n) => n);
243 }
244
245 /**
246 * Gets the table definitions
247 */
248 get tables() {
249 let stacks = [];
250
251 const defaultStack = this.getDefaultStack();
252 if (defaultStack) {
253 stacks.push(defaultStack);
254 }
255
256 if (this.hasAdditionalStacksPlugin()) {
257 stacks = stacks.concat(this.getAdditionalStacks());
258 }
259
260 return stacks.map((stack) => this.getTableDefinitionsFromStack(stack)).reduce((tables, tablesInStack) => tables.concat(tablesInStack), []);
261 }
262
263 /**
264 * Gets the seeding sources
265 */
266 get seedSources() {
267 const config = this.service.custom.dynamodb;
268 const seedConfig = _.get(config, "seed", {});
269 const seed = this.options.seed || config.start.seed || seedConfig;
270 let categories;
271 if (typeof seed === "string") {
272 categories = seed.split(",");
273 } else if(seed) {
274 categories = Object.keys(seedConfig);
275 } else { // if (!seed)
276 this.serverlessLog("DynamoDB - No seeding defined. Skipping data seeding.");
277 return [];
278 }
279 const sourcesByCategory = categories.map((category) => seedConfig[category].sources);
280 return [].concat.apply([], sourcesByCategory);
281 }
282
283 createTable(dynamodb, migration) {
284 return new BbPromise((resolve, reject) => {
285 if (migration.StreamSpecification && migration.StreamSpecification.StreamViewType) {
286 migration.StreamSpecification.StreamEnabled = true;
287 }
288 if (migration.TimeToLiveSpecification) {
289 delete migration.TimeToLiveSpecification;
290 }
291 if (migration.SSESpecification) {
292 migration.SSESpecification.Enabled = migration.SSESpecification.SSEEnabled;
293 delete migration.SSESpecification.SSEEnabled;
294 }
295 if (migration.PointInTimeRecoverySpecification) {
296 delete migration.PointInTimeRecoverySpecification;
297 }
298 if (migration.Tags) {
299 delete migration.Tags;
300 }
301 dynamodb.raw.createTable(migration, (err) => {
302 if (err) {
303 if (err.name === 'ResourceInUseException') {
304 this.serverlessLog(`DynamoDB - Warn - table ${migration.TableName} already exists`);
305 resolve();
306 } else {
307 this.serverlessLog("DynamoDB - Error - ", err);
308 reject(err);
309 }
310 } else {
311 this.serverlessLog("DynamoDB - created table " + migration.TableName);
312 resolve(migration);
313 }
314 });
315 });
316 }
317}
318module.exports = ServerlessDynamodbLocal;