@ftschopp/dynatable-migrations 1.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/.gitkeep +0 -0
- package/CHANGELOG.md +13 -0
- package/README.md +421 -0
- package/USAGE.md +557 -0
- package/bin/dynatable-migrate.js +30 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +123 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/down.d.ts +3 -0
- package/dist/commands/down.d.ts.map +1 -0
- package/dist/commands/down.js +37 -0
- package/dist/commands/down.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +84 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/up.d.ts +3 -0
- package/dist/commands/up.d.ts.map +1 -0
- package/dist/commands/up.js +37 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +160 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/loader.d.ts +47 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +226 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/runner.d.ts +38 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +166 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/tracker.d.ts +19 -0
- package/dist/core/tracker.d.ts.map +1 -0
- package/dist/core/tracker.js +172 -0
- package/dist/core/tracker.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/migration.d.ts +5 -0
- package/dist/templates/migration.d.ts.map +1 -0
- package/dist/templates/migration.js +96 -0
- package/dist/templates/migration.js.map +1 -0
- package/dist/types/index.d.ts +159 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/migrations/.gitkeep +0 -0
- package/package.json +43 -0
- package/src/cli.ts +106 -0
- package/src/commands/create.ts +110 -0
- package/src/commands/down.ts +42 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/status.ts +92 -0
- package/src/commands/up.ts +39 -0
- package/src/core/config.ts +140 -0
- package/src/core/loader.ts +233 -0
- package/src/core/runner.ts +223 -0
- package/src/core/tracker.ts +219 -0
- package/src/index.ts +23 -0
- package/src/templates/migration.ts +92 -0
- package/src/types/index.ts +196 -0
- package/tsconfig.json +19 -0
package/.gitkeep
ADDED
|
File without changes
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @ftschopp/dynatable-migrations 1.0.0 (2026-01-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* major initial release ([e282548](https://github.com/ftschopp/dynatable/commit/e28254895a40fdc26bab1dde2c634f663857ec16))
|
|
7
|
+
|
|
8
|
+
# Changelog
|
|
9
|
+
|
|
10
|
+
All notable changes to this project will be documented in this file.
|
|
11
|
+
|
|
12
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
13
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
package/README.md
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# @ftschopp/dynatable-migrations
|
|
2
|
+
|
|
3
|
+
DynamoDB migration tool for single table design with schema versioning.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Single Table Design** - Built specifically for DynamoDB single table design patterns
|
|
8
|
+
- 📊 **Schema Versioning** - Track schema evolution over time within your DynamoDB table
|
|
9
|
+
- 🔄 **Up/Down Migrations** - Support for both applying and rolling back migrations
|
|
10
|
+
- 📝 **Migration History** - All migration records stored in your DynamoDB table using Single Table Design
|
|
11
|
+
- 🎯 **TypeScript First** - Full TypeScript support with type safety
|
|
12
|
+
- 🛠️ **CLI Tool** - Easy-to-use command-line interface
|
|
13
|
+
- 📦 **Zero Dependencies** - Only requires AWS SDK
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @ftschopp/dynatable-migrations
|
|
19
|
+
# or
|
|
20
|
+
yarn add @ftschopp/dynatable-migrations
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Initialize
|
|
26
|
+
|
|
27
|
+
Create migration structure in your project:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx dynatable-migrate init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This creates:
|
|
34
|
+
|
|
35
|
+
- `migrations/` directory for your migration files
|
|
36
|
+
- `dynatable.config.js` configuration file
|
|
37
|
+
|
|
38
|
+
### 2. Configure
|
|
39
|
+
|
|
40
|
+
Edit `dynatable.config.js`:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
module.exports = {
|
|
44
|
+
tableName: 'MyTable',
|
|
45
|
+
client: {
|
|
46
|
+
region: 'us-east-1',
|
|
47
|
+
// For local DynamoDB
|
|
48
|
+
endpoint: 'http://localhost:8000',
|
|
49
|
+
credentials: {
|
|
50
|
+
accessKeyId: 'local',
|
|
51
|
+
secretAccessKey: 'local',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
migrationsDir: './migrations',
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Create Migration
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx dynatable-migrate create add_user_email
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This creates a file like `migrations/v0001_add_user_email.ts`
|
|
65
|
+
|
|
66
|
+
### 4. Edit Migration
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { Migration } from '@ftschopp/dynatable-migrations';
|
|
70
|
+
|
|
71
|
+
export const migration: Migration = {
|
|
72
|
+
version: 'v0001',
|
|
73
|
+
name: 'add_user_email',
|
|
74
|
+
description: 'Add email field to User entity',
|
|
75
|
+
|
|
76
|
+
async up({ client, tableName, dynamodb }) {
|
|
77
|
+
const { ScanCommand, UpdateCommand } = dynamodb;
|
|
78
|
+
|
|
79
|
+
// Scan all users
|
|
80
|
+
const result = await client.send(
|
|
81
|
+
new ScanCommand({
|
|
82
|
+
TableName: tableName,
|
|
83
|
+
FilterExpression: 'begins_with(PK, :pk)',
|
|
84
|
+
ExpressionAttributeValues: { ':pk': 'USER#' },
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Add email field to each user
|
|
89
|
+
for (const item of result.Items || []) {
|
|
90
|
+
await client.send(
|
|
91
|
+
new UpdateCommand({
|
|
92
|
+
TableName: tableName,
|
|
93
|
+
Key: { PK: item.PK, SK: item.SK },
|
|
94
|
+
UpdateExpression: 'SET email = :email, emailVerified = :verified',
|
|
95
|
+
ExpressionAttributeValues: {
|
|
96
|
+
':email': null,
|
|
97
|
+
':verified': false,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async down({ client, tableName, dynamodb }) {
|
|
105
|
+
const { ScanCommand, UpdateCommand } = dynamodb;
|
|
106
|
+
|
|
107
|
+
const result = await client.send(
|
|
108
|
+
new ScanCommand({
|
|
109
|
+
TableName: tableName,
|
|
110
|
+
FilterExpression: 'begins_with(PK, :pk)',
|
|
111
|
+
ExpressionAttributeValues: { ':pk': 'USER#' },
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
for (const item of result.Items || []) {
|
|
116
|
+
await client.send(
|
|
117
|
+
new UpdateCommand({
|
|
118
|
+
TableName: tableName,
|
|
119
|
+
Key: { PK: item.PK, SK: item.SK },
|
|
120
|
+
UpdateExpression: 'REMOVE email, emailVerified',
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 5. Run Migrations
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Apply all pending migrations
|
|
132
|
+
npx dynatable-migrate up
|
|
133
|
+
|
|
134
|
+
# Apply only 1 migration
|
|
135
|
+
npx dynatable-migrate up --limit 1
|
|
136
|
+
|
|
137
|
+
# Check status
|
|
138
|
+
npx dynatable-migrate status
|
|
139
|
+
|
|
140
|
+
# Rollback last migration
|
|
141
|
+
npx dynatable-migrate down
|
|
142
|
+
|
|
143
|
+
# Rollback last 2 migrations
|
|
144
|
+
npx dynatable-migrate down --steps 2
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## CLI Commands
|
|
148
|
+
|
|
149
|
+
### `init`
|
|
150
|
+
|
|
151
|
+
Initialize migrations in your project.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
dynatable-migrate init
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `create <name>`
|
|
158
|
+
|
|
159
|
+
Create a new migration file.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
dynatable-migrate create add_user_profile
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Options:
|
|
166
|
+
|
|
167
|
+
- `-c, --config <path>` - Custom config file path
|
|
168
|
+
|
|
169
|
+
### `up`
|
|
170
|
+
|
|
171
|
+
Run pending migrations.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
dynatable-migrate up
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Options:
|
|
178
|
+
|
|
179
|
+
- `-c, --config <path>` - Custom config file path
|
|
180
|
+
- `-l, --limit <number>` - Limit number of migrations to run
|
|
181
|
+
|
|
182
|
+
### `down`
|
|
183
|
+
|
|
184
|
+
Rollback migrations.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
dynatable-migrate down
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Options:
|
|
191
|
+
|
|
192
|
+
- `-c, --config <path>` - Custom config file path
|
|
193
|
+
- `-s, --steps <number>` - Number of migrations to rollback (default: 1)
|
|
194
|
+
|
|
195
|
+
### `status`
|
|
196
|
+
|
|
197
|
+
Show migration status.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
dynatable-migrate status
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Options:
|
|
204
|
+
|
|
205
|
+
- `-c, --config <path>` - Custom config file path
|
|
206
|
+
|
|
207
|
+
## How It Works
|
|
208
|
+
|
|
209
|
+
### Single Table Design
|
|
210
|
+
|
|
211
|
+
All migration tracking happens **within your DynamoDB table** using Single Table Design principles:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
PK SK version status appliedAt
|
|
215
|
+
-----------------------------------------------------------------------
|
|
216
|
+
_SCHEMA#VERSION v0001 v0001 applied 2025-03-29T10:00:00Z
|
|
217
|
+
_SCHEMA#VERSION v0002 v0002 applied 2025-03-29T11:00:00Z
|
|
218
|
+
_SCHEMA#VERSION#CURRENT _SCHEMA#VERSION v0002 - 2025-03-29T11:00:00Z
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A GSI (GSI1) is used to quickly find the current version:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
GSI1PK GSI1SK
|
|
225
|
+
---------------------------
|
|
226
|
+
_SCHEMA#CURRENT v0002
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Migration Context
|
|
230
|
+
|
|
231
|
+
Every migration receives a context object with:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface MigrationContext {
|
|
235
|
+
client: DynamoDBDocumentClient; // AWS SDK client
|
|
236
|
+
tableName: string; // Your table name
|
|
237
|
+
tracker: MigrationTracker; // Track schema changes
|
|
238
|
+
config: MigrationConfig; // Your config
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Schema Change Tracking
|
|
243
|
+
|
|
244
|
+
Track what changed in each migration:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
await tracker.recordSchemaChange({
|
|
248
|
+
entity: 'User',
|
|
249
|
+
changes: {
|
|
250
|
+
added: ['email', 'emailVerified'],
|
|
251
|
+
removed: ['oldField'],
|
|
252
|
+
modified: [{ field: 'status', from: 'string', to: 'enum' }],
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Migration Examples
|
|
258
|
+
|
|
259
|
+
### Add New Entity Type
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
export const migration: Migration = {
|
|
263
|
+
version: 'v0003',
|
|
264
|
+
name: 'add_comment_entity',
|
|
265
|
+
|
|
266
|
+
async up({ client, tableName }) {
|
|
267
|
+
// In Single Table Design, you typically don't need to do anything
|
|
268
|
+
// Just document the schema change
|
|
269
|
+
console.log('Comment entity added to schema');
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
async down({ client, tableName }) {
|
|
273
|
+
// Delete all comments if rolling back
|
|
274
|
+
const { ScanCommand, DeleteCommand } = await import('@aws-sdk/lib-dynamodb');
|
|
275
|
+
|
|
276
|
+
const result = await client.send(
|
|
277
|
+
new ScanCommand({
|
|
278
|
+
TableName: tableName,
|
|
279
|
+
FilterExpression: 'begins_with(PK, :pk)',
|
|
280
|
+
ExpressionAttributeValues: { ':pk': 'COMMENT#' },
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
for (const item of result.Items || []) {
|
|
285
|
+
await client.send(
|
|
286
|
+
new DeleteCommand({
|
|
287
|
+
TableName: tableName,
|
|
288
|
+
Key: { PK: item.PK, SK: item.SK },
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Change Key Structure
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
export const migration: Migration = {
|
|
300
|
+
version: 'v0004',
|
|
301
|
+
name: 'change_photo_sort_key',
|
|
302
|
+
|
|
303
|
+
async up({ client, tableName }) {
|
|
304
|
+
// Change from SK: "PHOTO#${id}" to SK: "PHOTO#${timestamp}#${id}"
|
|
305
|
+
const { ScanCommand, TransactWriteCommand } = await import('@aws-sdk/lib-dynamodb');
|
|
306
|
+
|
|
307
|
+
const result = await client.send(
|
|
308
|
+
new ScanCommand({
|
|
309
|
+
TableName: tableName,
|
|
310
|
+
FilterExpression: 'begins_with(SK, :sk)',
|
|
311
|
+
ExpressionAttributeValues: { ':sk': 'PHOTO#' },
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
for (const photo of result.Items || []) {
|
|
316
|
+
const timestamp = new Date(photo.createdAt).getTime();
|
|
317
|
+
const photoId = photo.SK.replace('PHOTO#', '');
|
|
318
|
+
|
|
319
|
+
await client.send(
|
|
320
|
+
new TransactWriteCommand({
|
|
321
|
+
TransactItems: [
|
|
322
|
+
{
|
|
323
|
+
Delete: {
|
|
324
|
+
TableName: tableName,
|
|
325
|
+
Key: { PK: photo.PK, SK: photo.SK },
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
Put: {
|
|
330
|
+
TableName: tableName,
|
|
331
|
+
Item: {
|
|
332
|
+
...photo,
|
|
333
|
+
SK: `PHOTO#${timestamp}#${photoId}`,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
async down({ client, tableName }) {
|
|
344
|
+
// Reverse the change
|
|
345
|
+
// ... similar logic but reverse
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Configuration
|
|
351
|
+
|
|
352
|
+
### Config File Options
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
interface MigrationConfig {
|
|
356
|
+
// Required: DynamoDB table name
|
|
357
|
+
tableName: string;
|
|
358
|
+
|
|
359
|
+
// Required: AWS client config
|
|
360
|
+
client: {
|
|
361
|
+
region: string;
|
|
362
|
+
endpoint?: string; // For local DynamoDB
|
|
363
|
+
credentials?: {
|
|
364
|
+
accessKeyId: string;
|
|
365
|
+
secretAccessKey: string;
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Optional: Migrations directory (default: ./migrations)
|
|
370
|
+
migrationsDir?: string;
|
|
371
|
+
|
|
372
|
+
// Optional: Tracking prefix (default: _SCHEMA#VERSION)
|
|
373
|
+
trackingPrefix?: string;
|
|
374
|
+
|
|
375
|
+
// Optional: GSI name (default: GSI1)
|
|
376
|
+
gsi1Name?: string;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Programmatic Usage
|
|
381
|
+
|
|
382
|
+
You can also use the migration runner programmatically:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { MigrationRunner, loadConfig } from '@ftschopp/dynatable-migrations';
|
|
386
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
387
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
388
|
+
|
|
389
|
+
const config = await loadConfig();
|
|
390
|
+
const client = DynamoDBDocumentClient.from(
|
|
391
|
+
new DynamoDBClient({
|
|
392
|
+
region: config.client.region,
|
|
393
|
+
endpoint: config.client.endpoint,
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const runner = new MigrationRunner(client, config);
|
|
398
|
+
|
|
399
|
+
// Run migrations
|
|
400
|
+
await runner.up();
|
|
401
|
+
|
|
402
|
+
// Get status
|
|
403
|
+
const status = await runner.status();
|
|
404
|
+
|
|
405
|
+
// Rollback
|
|
406
|
+
await runner.down(1);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Best Practices
|
|
410
|
+
|
|
411
|
+
1. **Always write down() functions** - Even if you think you won't need to rollback
|
|
412
|
+
2. **Test migrations locally first** - Use DynamoDB Local
|
|
413
|
+
3. **Backup before running** - Use DynamoDB point-in-time recovery
|
|
414
|
+
4. **One change per migration** - Keep migrations focused
|
|
415
|
+
5. **Don't modify applied migrations** - Create a new migration instead
|
|
416
|
+
6. **Use transactions** - For multi-step changes that must be atomic
|
|
417
|
+
7. **Document schema changes** - Use the schema snapshot field
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|