Skip to content

Commit 9a6a062

Browse files
authored
Merge pull request #8 from objectwow/dev
Merge to main
2 parents 066abf9 + 48f7c76 commit 9a6a062

9 files changed

Lines changed: 888 additions & 240 deletions

File tree

README.md

Lines changed: 194 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,107 @@ In a microservices system where each service owns its own database, querying dat
1212
npm i @objectwow/join
1313
```
1414

15-
## Basic usage
15+
## Usage
1616

1717
```typescript
1818
import { joinData } from "@objectwow/join";
1919

20-
const local = [
21-
{ id: 1, items: [101, 102] },
22-
{ id: 2, items: [201] },
20+
const orders = [
21+
{
22+
id: 1,
23+
code: "1",
24+
fulfillments: [
25+
{
26+
id: 11,
27+
code: "11",
28+
products: [
29+
{
30+
id: 111,
31+
quantity: 1,
32+
},
33+
{
34+
id: 112,
35+
quantity: 4,
36+
},
37+
],
38+
},
39+
{
40+
id: 12,
41+
code: "12",
42+
products: [
43+
{
44+
id: 111,
45+
quantity: 8,
46+
},
47+
],
48+
},
49+
],
50+
},
51+
{
52+
id: 2,
53+
code: "2",
54+
fulfillments: [
55+
{
56+
id: 21,
57+
code: "21",
58+
products: [
59+
{
60+
id: 111,
61+
quantity: 9,
62+
},
63+
{
64+
id: 112,
65+
quantity: 7,
66+
},
67+
],
68+
},
69+
{
70+
id: 22,
71+
code: "22",
72+
products: [
73+
{
74+
id: 111,
75+
quantity: 2,
76+
},
77+
{
78+
id: 112,
79+
quantity: 3,
80+
},
81+
],
82+
},
83+
],
84+
},
2385
];
2486

25-
const from = () => [
26-
{ id: 101, product: "Product 101" },
27-
{ id: 102, product: "Product 102" },
28-
{ id: 201, product: "Product 201" },
87+
const products = () => [
88+
{
89+
id: 111,
90+
name: "Product 1",
91+
price: 10,
92+
},
93+
{
94+
id: 112,
95+
name: "Product 2",
96+
price: 20,
97+
},
98+
{
99+
id: 113,
100+
name: "Product 3",
101+
price: 30,
102+
},
29103
];
30104

31105
const result = await joinData({
32-
local,
33-
localField: "items",
34-
from,
106+
local: orders,
107+
from: () => products,
108+
localField: "fulfillments.products.id",
35109
fromField: "id",
36-
as: "products",
110+
as: "fulfillments.products",
111+
asMap: {
112+
id: "id",
113+
name: "name",
114+
price: "price",
115+
},
37116
});
38117
```
39118

@@ -43,16 +122,44 @@ LocalData will be overwritten
43122
local = [
44123
{
45124
id: 1,
46-
items: [101, 102],
47-
products: [
48-
{ id: 101, product: "Product 101" },
49-
{ id: 102, product: "Product 102" },
125+
code: "1",
126+
fulfillments: [
127+
{
128+
id: 11,
129+
code: "11",
130+
products: [
131+
{ id: 111, name: "Product 1", price: 10, quantity: 1 },
132+
{ id: 112, name: "Product 2", price: 20, quantity: 4 },
133+
],
134+
},
135+
{
136+
id: 12,
137+
code: "12",
138+
products: [{ id: 111, name: "Product 1", price: 10, quantity: 8 }],
139+
},
50140
],
51141
},
52142
{
53143
id: 2,
54-
items: [201],
55-
products: [{ id: 201, product: "Product 201" }],
144+
code: "2",
145+
fulfillments: [
146+
{
147+
id: 21,
148+
code: "21",
149+
products: [
150+
{ id: 111, name: "Product 1", price: 10, quantity: 9 },
151+
{ id: 112, name: "Product 2", price: 20, quantity: 7 },
152+
],
153+
},
154+
{
155+
id: 22,
156+
code: "22",
157+
products: [
158+
{ id: 111, name: "Product 1", price: 10, quantity: 2 },
159+
{ id: 112, name: "Product 2", price: 20, quantity: 3 },
160+
],
161+
},
162+
],
56163
},
57164
];
58165

@@ -62,53 +169,66 @@ result = {
62169
};
63170
```
64171

65-
Note: see more samples in the [`tests`](https://github.com/objectwow/join/blob/main/tests/core.spec.ts) and ['test-by-cases](https://github.com/objectwow/join/blob/main/test-by-cases)
172+
Note: see more samples in the [`tests`](https://github.com/objectwow/join/blob/main/tests/core.spec.ts) and [`test-by-cases`](https://github.com/objectwow/join/blob/main/test-by-cases)
66173

67174
```typescript
175+
/**
176+
* Parameters for the `joinData` function to perform joins between local data and source data.
177+
*/
68178
export interface JoinDataParam {
69179
/**
70-
* Local object or an array of local objects to be joined.
180+
* Local object or array of local objects to be joined.
71181
*/
72182
local: LocalParam;
73183

74184
/**
75-
* A callback (async) function that returns the data from the source.
76-
* Data is object or an array of objects
185+
* Object(s) or an asynchronous callback function that returns the data from the source.
77186
*/
78-
from: (...args: any[]) => any;
187+
from: FromParam;
79188

80189
/**
81-
* The field name in the local object(s) used for the join,
82-
* can be a nested field, separated by a dot ('.')
190+
* Field name in the local object(s) used for the join.
83191
*/
84192
localField: string;
85193

86194
/**
87-
* The field name in the from object used for the join,
88-
* can be a nested field, separated by a dot ('.')
195+
* Field name in the `from` object(s) used for the join.
89196
*/
90197
fromField: string;
91198

92199
/**
93-
* An optional new field name to store the result of the join in the local object(s).
200+
* Optional new field name to store the result of the join in the local object(s).
94201
*/
95202
as?: string;
96203

97204
/**
98-
* An optional mapping from the from object(s) values to the new field names in the local object(s).
205+
* Optional mapping from the `fromField` values to new field names in the local object(s).
99206
*/
100207
asMap?: AsMap;
101208
}
102209

103-
export type LocalParam = object | object[];
104-
105-
export type AsMap = { [key: string]: string };
210+
export type FromParam =
211+
| ((localFieldValues: Primitive[], metadata: any) => any)
212+
| object
213+
| object[];
106214

107-
export type JoinDataResult =
108-
| { joinFailedValues: Primitive[]; allSuccess: boolean }
109-
| any;
215+
export type AsMap =
216+
| ((currentFrom: any, currentLocal: any, metadata: any) => any)
217+
| { [key: string]: string }
218+
| string;
110219
```
111220

221+
## Test
222+
223+
`npm run test` or `npm run test:cov`
224+
225+
| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
226+
| ------------ | ------- | -------- | ------- | ------- | ------------------------------------ |
227+
| All files | 93 | 78.68 | 95.23 | 92.8 | |
228+
| core.ts | 90.82 | 73.17 | 100 | 90.82 | 18,42,53,106,128,141,163,206,247,251 |
229+
| singleton.ts | 100 | 100 | 75 | 100 | |
230+
| util.ts | 100 | 89.47 | 100 | 100 | 23-24 |
231+
112232
## Customization
113233

114234
With an out-of-the-box design, you can create your own function using the current structure.
@@ -117,32 +237,16 @@ With an out-of-the-box design, you can create your own function using the curren
117237
import { JoinData } from "@objectwow/join";
118238

119239
export class YourJoin extends JoinData {
120-
public execute(
121-
param: JoinDataParam,
122-
metadata?: any
123-
): Promise<JoinDataResult> {}
124-
125240
// Use case: Currently, deep values are split by a dot ('.'), but you can use a different symbol if needed
126241
protected separateSymbol: string;
127242

128-
protected generateAsValue(param: GenerateAsValueParam) {}
129-
130243
// Use case: Return your custom output
131244
protected generateResult(
132245
joinFailedValues: Primitive[],
133246
localOverwrite: LocalParam,
134247
metadata?: any
135248
) {}
136249

137-
protected getFieldValue(parent: object, path: string) {}
138-
139-
protected handleLocalObj(param: HandleLocalObjParam): void {}
140-
141-
protected parseFieldPath(fieldPath: string): {
142-
path: string;
143-
newPath: string;
144-
} {}
145-
146250
// Use case: Shadow clone local data without overwriting the original.
147251
protected standardizeLocalParam(
148252
local: LocalParam,
@@ -152,6 +256,7 @@ export class YourJoin extends JoinData {
152256
// Use case: Automatically call internal or external services to retrieve data based on the input
153257
protected standardizeFromParam(
154258
from: FromParam,
259+
localFieldValues: string[],
155260
metadata?: any
156261
): Promise<any[]> {}
157262

@@ -199,11 +304,45 @@ await joinCls.execute({...})
199304

200305
Tips:
201306

202-
- You can call original function (parent function) with `super.`
307+
- You can call original function (parent function) with `super.
203308
- You can pass anything to the metadata
204309

205310
## Benchmark
206311

207-
- Execute: `node lib/benchmark.js`
208-
- Report: JoinData Execution x 2,289,754 ops/sec ±3.38% (86 runs sampled)
209-
- Device: MacbookPro M1, 16 GB RAM, 12 CPU
312+
- Source:
313+
314+
```typescript
315+
// Generate localData with 100 orders and 2 fulfillments per order
316+
const localData = Array.from({ length: 100 }, (_, i) => ({
317+
id: i + 1, // Order ID starting from 1
318+
code: `${i + 1}`, // Order code
319+
fulfillments: Array.from({ length: 2 }, (_, j) => ({
320+
id: (i + 1) * 10 + j, // Fulfillment ID for each order
321+
code: `${(i + 1) * 10 + j}`, // Fulfillment code
322+
products: Array.from({ length: 2 }, () => {
323+
const productId = Math.floor(Math.random() * 100) + 111; // Random product ID between 111 and 210
324+
return {
325+
id: productId,
326+
quantity: Math.floor(Math.random() * 10) + 1, // Random quantity between 1 and 10
327+
};
328+
}),
329+
})),
330+
}));
331+
332+
// Generate fromData with 100 products
333+
const fromData = Array.from({ length: 100 }, (_, i) => ({
334+
id: i + 111, // IDs starting from 111
335+
name: `Product ${i + 1}`,
336+
price: Math.floor(Math.random() * 100 + 1), // Random price between 1 and 100
337+
}));
338+
```
339+
340+
- Execute: `node lib/benchmark.js` (need build first)
341+
- Report: JoinData Execution x 125,972 ops/sec ±1.27% (66 runs sampled)
342+
- Device: Macbook Pro M1 Pro, 16 GB RAM, 12 CPU
343+
344+
## Contributors
345+
346+
If you have any questions, feel free to open an [`open an issue on GitHub`](https://github.com/objectwow/join/issues) or connect with me on [`Linkedin`](https://www.linkedin.com/in/vtuanjs/).
347+
348+
Thank you for using and supporting the project!

benchmark.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,44 @@ import { joinData } from "./index"; // Import your core function/module
55

66
const suite = new Benchmark.Suite();
77

8-
// Example input data
9-
const localData = { id: 1, name: "LocalName" };
10-
const fromData = [{ id: 1, name: "Alice", age: 25 }];
8+
const localData = Array.from({ length: 100 }, (_, i) => ({
9+
id: i + 1, // Order ID starting from 1
10+
code: `${i + 1}`, // Order code
11+
fulfillments: Array.from({ length: 2 }, (_, j) => ({
12+
id: (i + 1) * 10 + j, // Fulfillment ID for each order
13+
code: `${(i + 1) * 10 + j}`, // Fulfillment code
14+
products: Array.from({ length: 2 }, () => {
15+
const productId = Math.floor(Math.random() * 100) + 111; // Random product ID between 111 and 210
16+
return {
17+
id: productId,
18+
quantity: Math.floor(Math.random() * 10) + 1, // Random quantity between 1 and 10
19+
};
20+
}),
21+
})),
22+
}));
23+
24+
// Generate fromData with 100 products
25+
const fromData = Array.from({ length: 100 }, (_, i) => ({
26+
id: i + 111, // IDs starting from 111
27+
name: `Product ${i + 1}`,
28+
price: Math.floor(Math.random() * 100 + 1), // Random price between 1 and 100
29+
}));
1130

1231
// Add your benchmarks
1332
suite
1433
.add("JoinData Execution", () => {
1534
// Call the method you want to benchmark
1635
joinData({
17-
local: localData,
18-
from: () => fromData,
19-
localField: "id",
36+
local: JSON.parse(JSON.stringify(localData)),
37+
from: fromData,
38+
localField: "fulfillments.products.id",
2039
fromField: "id",
21-
asMap: undefined,
22-
as: "from",
40+
as: "fulfillments.products",
41+
asMap: {
42+
id: "id",
43+
name: "name",
44+
price: "price",
45+
},
2346
});
2447
})
2548
.on("cycle", (event: any) => {

0 commit comments

Comments
 (0)