Skip to content

Commit d0ad75b

Browse files
CopilotTechQuery
andauthored
[add] complete MobX-RESTful-migrator TypeScript package with modern tooling (#1)
Co-authored-by: South Drifter <[email protected]>
1 parent 26b00f9 commit d0ad75b

File tree

17 files changed

+4350
-1
lines changed

17 files changed

+4350
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ out
8282
.nuxt
8383
dist
8484

85+
# NPM lock file
86+
package-lock.json
87+
yarn.lock
88+
8589
# Gatsby files
8690
.cache/
8791
# Comment in the public line in if your project uses Gatsby and not Next.js

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
auto-install-peers = false

README.md

Lines changed: 333 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,334 @@
11
# MobX-RESTful-migrator
2-
Data Migration framework based on MobX-RESTful
2+
3+
Data Migration framework based on [MobX-RESTful][1]
4+
5+
## Overview
6+
7+
MobX-RESTful-migrator is a TypeScript library that provides a flexible data migration framework built on top of MobX-RESTful's ListModel abstraction. It allows you to migrate data from various sources through MobX-RESTful models with customizable field mappings and relationships.
8+
9+
## Features
10+
11+
- **Flexible Field Mappings**: Support for four different mapping types
12+
- **Async Generator Pattern**: Control migration flow at your own pace
13+
- **Cross-table Relationships**: Handle complex data relationships
14+
- **Event-Driven Architecture**: Built-in console logging with customizable event bus
15+
- **TypeScript Support**: Full TypeScript support with type safety
16+
17+
## Installation
18+
19+
```bash
20+
npm install mobx-restful mobx-restful-migrator
21+
```
22+
23+
## Usage Example: Article migration
24+
25+
The typical use case is migrating Article data with the following schema:
26+
27+
- **Source**: Article table with Title, Keywords, Content, Author, Email fields
28+
- **Target**: Keywords field splits into Category string & Tags array, Author & Email fields map to User table
29+
30+
### Source Data Schema
31+
32+
```typescript
33+
interface SourceArticle {
34+
id: number;
35+
title: string;
36+
subtitle: string;
37+
keywords: string; // comma-separated keywords to split into category & tags
38+
content: string;
39+
author: string; // maps to User table "name" field
40+
email: string; // maps to User table "email" field
41+
}
42+
```
43+
44+
### Target Models
45+
46+
```typescript
47+
import { HTTPClient } from 'koajax';
48+
import { ListModel, DataObject, Filter, IDType, toggle } from 'mobx-restful';
49+
import { buildURLData } from 'web-utility';
50+
51+
export abstract class TableModel<
52+
D extends DataObject,
53+
F extends Filter<D> = Filter<D>
54+
> extends ListModel<D, F> {
55+
client = new HTTPClient({ baseURI: 'http://localhost:8080', responseType: 'json' });
56+
57+
@toggle('uploading')
58+
async updateOne(data: Filter<D>, id?: IDType) {
59+
const { body } = await (id
60+
? this.client.put<D>(`${this.baseURI}/${id}`, data)
61+
: this.client.post<D>(this.baseURI, data));
62+
63+
return (this.currentOne = body!);
64+
}
65+
66+
async loadPage(pageIndex: number, pageSize: number, filter: F) {
67+
const { body } = await this.client.get<{ list: D[]; count: number }>(
68+
`${this.baseURI}?${buildURLData({ ...filter, pageIndex, pageSize })}`
69+
);
70+
return { pageData: body!.list, totalCount: body!.count };
71+
}
72+
}
73+
74+
export interface User {
75+
id: number;
76+
name: string;
77+
email?: string;
78+
}
79+
80+
export interface Article {
81+
id: number;
82+
title: string;
83+
category: string;
84+
tags: string[];
85+
content: string;
86+
author: User;
87+
}
88+
89+
export class UserModel extends TableModel<User> {
90+
baseURI = '/users';
91+
}
92+
93+
export class ArticleModel extends TableModel<Article> {
94+
baseURI = '/articles';
95+
}
96+
```
97+
98+
### Migration Configuration
99+
100+
First, export your CSV data file `articles.csv` from an Excel file or Old database:
101+
102+
```csv
103+
title,subtitle,keywords,content,author,email
104+
Introduction to TypeScript,A Comprehensive Guide,"typescript,javascript,programming","TypeScript is a typed superset of JavaScript...",John Doe,[email protected]
105+
MobX State Management,Made Simple,"mobx,react,state-management","MobX makes state management simple...",Jane Smith,[email protected]
106+
```
107+
108+
Then implement the migration:
109+
110+
```typescript
111+
#! /usr/bin/env tsx
112+
113+
import { RestMigrator, MigrationSchema, ConsoleLogger } from 'mobx-restful-migrator';
114+
import { FileHandle, open } from 'fs/promises';
115+
import { readTextTable } from 'web-utility';
116+
117+
import { SourceArticle, Article, ArticleModel, UserModel } from './source';
118+
119+
// Load and parse CSV data using async streaming for large files
120+
async function* readCSV<T extends object>(path: string) {
121+
let fileHandle: FileHandle | undefined;
122+
123+
try {
124+
fileHandle = await open(path);
125+
126+
const stream = fileHandle.createReadStream({ encoding: 'utf-8' });
127+
128+
yield* readTextTable<T>(stream, true) as AsyncGenerator<T>;
129+
} finally {
130+
await fileHandle?.close();
131+
}
132+
}
133+
134+
const loadSourceArticles = () => readCSV<SourceArticle>('article.csv');
135+
136+
// Complete migration configuration demonstrating all 4 mapping types
137+
const mapping: MigrationSchema<SourceArticle, Article> = {
138+
139+
// 1. Many-to-One mapping: Title + Subtitle → combined title
140+
title: ({ title, subtitle }) => ({
141+
title: { value: `${title}: ${subtitle}` },
142+
}),
143+
content: 'content',
144+
145+
// 2. One-to-Many mapping: Keywords string → category string & tags array
146+
keywords: ({ keywords }) => {
147+
const [category, ...tags] = keywords.split(',').map(tag => tag.trim());
148+
149+
return { category: { value: category }, tags: { value: tags } };
150+
},
151+
// 3. Cross-table relationship: Author & Email → User table
152+
author: ({ author, email }) => ({
153+
author: {
154+
value: { name: author, email },
155+
model: UserModel, // Maps to User table via ListModel
156+
},
157+
}),
158+
};
159+
160+
// Run migration with built-in console logging (default)
161+
const migrator = new RestMigrator(loadSourceArticles, ArticleModel, mapping);
162+
163+
// The ConsoleLogger automatically logs each step:
164+
// - saved No.X: successful migrations with source, mapped, and target data
165+
// - skipped No.X: skipped items (duplicate unique fields)
166+
// - error at No.X: migration errors with details
167+
168+
for await (const { title } of migrator.boot()) {
169+
// Process the migrated target objects
170+
console.log(`Successfully migrated article: ${title}`);
171+
}
172+
```
173+
174+
In the end, run your script with a TypeScript runtime:
175+
176+
```bash
177+
tsx your-migration.ts 1> saved.log 2> error.log
178+
```
179+
180+
### Optional: Use custom event bus
181+
182+
```typescript
183+
class CustomEventBus implements MigrationEventBus<SourceArticle, Article> {
184+
async save({ index, targetItem }) {
185+
console.info(`✅ Migrated article ${index}: ${targetItem?.title}`);
186+
}
187+
188+
async skip({ index, error }) {
189+
console.warn(`⚠️ Skipped article ${index}: ${error?.message}`);
190+
}
191+
192+
async error({ index, error }) {
193+
console.error(`❌ Error at article ${index}: ${error?.message}`);
194+
}
195+
}
196+
197+
const migratorWithCustomLogger = new RestMigrator(
198+
loadSourceArticles,
199+
ArticleModel,
200+
mapping,
201+
new CustomEventBus()
202+
);
203+
```
204+
205+
## Four Mapping Types
206+
207+
### 1. Simple 1-to-1 Mapping
208+
209+
Map source field directly to target field using string mapping:
210+
211+
```typescript
212+
const mapping: MigrationSchema<SourceArticle, Article> = {
213+
title: 'title',
214+
content: 'content',
215+
};
216+
```
217+
218+
### 2. Many-to-One Mapping
219+
220+
Use resolver function to combine multiple source fields into one target field:
221+
222+
```typescript
223+
const mapping: MigrationSchema<SourceArticle, Article> = {
224+
title: ({ title, subtitle }) => ({
225+
title: { value: `${title}: ${subtitle}` },
226+
}),
227+
};
228+
```
229+
230+
### 3. One-to-Many Mapping
231+
232+
Use resolver function to map one source field to multiple target fields with `value` property:
233+
234+
```typescript
235+
const mapping: MigrationSchema<SourceArticle, Article> = {
236+
keywords: ({ keywords }) => {
237+
const [category, ...tags] = keywords.split(',').map(tag => tag.trim());
238+
239+
return { category: { value: category }, tags: { value: tags } };
240+
},
241+
};
242+
```
243+
244+
### 4. Cross-Table Relationships
245+
246+
Use resolver function with `model` property for related tables:
247+
248+
```typescript
249+
const mapping: MigrationSchema<SourceArticle, Article> = {
250+
author: ({ author, email }) => ({
251+
author: {
252+
value: { name: author, email },
253+
model: UserModel, // References User ListModel
254+
},
255+
}),
256+
};
257+
```
258+
259+
## Event-Driven Migration Architecture
260+
261+
The migrator includes a built-in Event Bus for monitoring and controlling the migration process:
262+
263+
### Built-in Console Logging
264+
265+
By default, RestMigrator uses the `ConsoleLogger` which provides detailed console output:
266+
267+
```typescript
268+
import { RestMigrator, ConsoleLogger } from 'mobx-restful-migrator';
269+
270+
import { loadSourceArticles, ArticleModel, mapping } from './source';
271+
272+
// ConsoleLogger is used by default
273+
const migrator = new RestMigrator(loadSourceArticles, ArticleModel, mapping);
274+
275+
for await (const { title } of migrator.boot()) {
276+
// Console automatically shows:
277+
// - saved No.X with source, mapped, and target data tables
278+
// - skipped No.X for duplicate unique fields
279+
// - error at No.X for migration errors
280+
281+
// Your processing logic here
282+
console.log(`✅ Article migrated: ${title}`);
283+
}
284+
```
285+
286+
### Custom Event Handling
287+
288+
Implement your own Event Bus for custom logging and monitoring:
289+
290+
```typescript
291+
import { MigrationEventBus, MigrationProgress } from 'mobx-restful-migrator';
292+
import { outputJSON } from 'fs-extra';
293+
294+
import { SourceArticle, Article, loadSourceArticles, ArticleModel, mapping } from './source';
295+
296+
class FileLogger implements MigrationEventBus<SourceArticle, Article> {
297+
bootedAt = new Date().toJSON();
298+
299+
async save({ index, sourceItem, targetItem }: MigrationProgress<SourceArticle, Article>) {
300+
// Log to file, send notifications, etc.
301+
await outputJSON(`logs/save-${this.bootedAt}.json`, {
302+
type: 'success',
303+
index,
304+
sourceId: sourceItem?.id,
305+
targetId: targetItem?.id,
306+
savedAt: new Date().toJSON(),
307+
});
308+
}
309+
310+
async skip({ index, sourceItem, error }: MigrationProgress<SourceArticle, Article>) {
311+
await outputJSON(`logs/skip-${this.bootedAt}.json`, {
312+
type: 'skipped',
313+
index,
314+
sourceId: sourceItem?.id,
315+
error: error?.message,
316+
skippedAt: new Date().toJSON(),
317+
});
318+
}
319+
320+
async error({ index, sourceItem, error }: MigrationProgress<SourceArticle, Article>) {
321+
await outputJSON(`logs/error-${this.bootedAt}.json`, {
322+
type: 'error',
323+
index,
324+
sourceId: sourceItem?.id,
325+
error: error?.message,
326+
errorAt: new Date().toJSON(),
327+
});
328+
}
329+
}
330+
331+
const migrator = new RestMigrator(loadSourceArticles, ArticleModel, mapping, new FileLogger());
332+
```
333+
334+
[1]: https://github.com/idea2app/MobX-RESTful

0 commit comments

Comments
 (0)