relation-matcher 1.0.12 → 1.1.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/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "relation-matcher",
3
- "version": "1.0.12",
4
- "description": "A utility to convert table data (such as out of a SQL query) into structured JSON.",
5
- "license": "ISC",
6
- "author": "",
7
- "type": "module",
8
- "main": "dist/src/index.js",
9
- "types": "dist/src/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "default": "./dist/src/index.js",
13
- "types": "./dist/src/index.d.ts"
14
- },
15
- "./types": {
16
- "default": "./dist/src/types/index.js",
17
- "types": "./dist/src/types/index.d.ts"
18
- },
19
- "./source": "./src/index.ts",
20
- "./source/types": "./src/types/index.ts"
21
- },
22
- "scripts": {
23
- "test": "jest && tsc --noEmit",
24
- "file-output": "tsx --watch src/file-output.ts",
25
- "build": "tsc",
26
- "build:publish": "yarn run test && yarn run build && npm publish"
27
- },
28
- "packageManager": "yarn@4.13.0",
29
- "devDependencies": {
30
- "@types/jest": "^30.0.0",
31
- "@types/node": "^25.0.10",
32
- "jest": "^30.2.0",
33
- "ts-jest": "^29.4.6",
34
- "typescript": "^5.9.3"
35
- }
36
- }
1
+ {
2
+ "name": "relation-matcher",
3
+ "version": "1.1.0",
4
+ "description": "A utility to convert table data (such as out of a SQL query) into structured JSON.",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "dist/src/index.js",
9
+ "types": "dist/src/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "default": "./dist/src/index.js",
13
+ "types": "./dist/src/index.d.ts"
14
+ },
15
+ "./types": {
16
+ "default": "./dist/src/types/index.js",
17
+ "types": "./dist/src/types/index.d.ts"
18
+ },
19
+ "./source": "./src/index.ts",
20
+ "./source/types": "./src/types/index.ts"
21
+ },
22
+ "scripts": {
23
+ "test": "jest && tsc --noEmit",
24
+ "file-output": "tsx --watch src/file-output.ts",
25
+ "build": "tsc",
26
+ "build:publish": "yarn run test && yarn run build && npm publish"
27
+ },
28
+ "packageManager": "yarn@4.13.0",
29
+ "devDependencies": {
30
+ "@types/jest": "^30.0.0",
31
+ "@types/node": "^25.0.10",
32
+ "jest": "^30.2.0",
33
+ "ts-jest": "^29.4.6",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }
package/src/index.test.ts CHANGED
@@ -1,105 +1,105 @@
1
- import { relationMatcherRoot } from ".";
2
- import { testData, testSchema } from "./testing/test-data";
3
-
4
- test("Tests output of mapper.", () => {
5
- const relationMatcherData = relationMatcherRoot(testData, testSchema);
6
-
7
- console.log(JSON.stringify(relationMatcherData, null, "\t"));
8
-
9
- expect(relationMatcherData).toStrictEqual({
10
- "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01": {
11
- id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
12
- clerkId: "user_abc123",
13
- email: "alice@example.com",
14
- createdAt: "2025-01-12T09:41:22.000Z",
15
-
16
- teamToUsers: [
17
- {
18
- teamId: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
19
- userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
20
- role: "admin",
21
- team: {
22
- id: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
23
- name: "Red Dragons",
24
- },
25
- },
26
- {
27
- teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
28
- userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
29
- role: "member",
30
- team: {
31
- id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
32
- name: "Blue Sharks",
33
- },
34
- },
35
- ],
36
-
37
- posts: [
38
- {
39
- id: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
40
- title: "First post",
41
- published: true,
42
- userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
43
- comments: [
44
- {
45
- id: "9d1a7c3b-2f6a-4f7c-bf4b-8f6e3c5d9e01",
46
- body: "Nice post!",
47
- postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
48
- authorEmail: "bob@example.com",
49
- },
50
- {
51
- id: "e3c9b5a2-7f42-4b7e-9e3d-4a6f1d8b2c44",
52
- body: "Thanks for sharing",
53
- postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
54
- authorEmail: "charlie@example.com",
55
- },
56
- ],
57
- },
58
- {
59
- id: "6bcb2b74-9b2f-4b38-bdb5-77c2e63d9c10",
60
- title: "Second post",
61
- published: false,
62
- userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
63
- comments: [],
64
- },
65
- ],
66
- },
67
- "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55": {
68
- id: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
69
- clerkId: "user_xyz789",
70
- email: "dave@example.com",
71
- createdAt: "2025-02-03T14:18:10.000Z",
72
-
73
- teamToUsers: [
74
- {
75
- teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
76
- userId: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
77
- role: "admin",
78
- team: {
79
- id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
80
- name: "Blue Sharks",
81
- },
82
- },
83
- ],
84
-
85
- posts: [],
86
- },
87
- });
88
- });
89
-
90
- test("Tests empty input.", () => {
91
- type InputRow = {
92
- user: {
93
- clerk_id: string;
94
- };
95
- };
96
-
97
- const input: InputRow[] = [];
98
-
99
- expect(
100
- relationMatcherRoot(input, {
101
- base: "user",
102
- id: "clerk_id",
103
- }),
104
- ).toStrictEqual({});
105
- });
1
+ import { relationMatcherRoot } from ".";
2
+ import { testData, testSchema } from "./testing/test-data";
3
+
4
+ test("Tests output of mapper.", () => {
5
+ const relationMatcherData = relationMatcherRoot(testData, testSchema);
6
+
7
+ console.log(JSON.stringify(relationMatcherData, null, "\t"));
8
+
9
+ expect(relationMatcherData).toStrictEqual({
10
+ "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01": {
11
+ id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
12
+ clerkId: "user_abc123",
13
+ email: "alice@example.com",
14
+ createdAt: "2025-01-12T09:41:22.000Z",
15
+
16
+ teamToUsers: [
17
+ {
18
+ teamId: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
19
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
20
+ role: "admin",
21
+ team: {
22
+ id: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
23
+ name: "Red Dragons",
24
+ },
25
+ },
26
+ {
27
+ teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
28
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
29
+ role: "member",
30
+ team: {
31
+ id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
32
+ name: "Blue Sharks",
33
+ },
34
+ },
35
+ ],
36
+
37
+ posts: [
38
+ {
39
+ id: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
40
+ title: "First post",
41
+ published: true,
42
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
43
+ comments: [
44
+ {
45
+ id: "9d1a7c3b-2f6a-4f7c-bf4b-8f6e3c5d9e01",
46
+ body: "Nice post!",
47
+ postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
48
+ authorEmail: "bob@example.com",
49
+ },
50
+ {
51
+ id: "e3c9b5a2-7f42-4b7e-9e3d-4a6f1d8b2c44",
52
+ body: "Thanks for sharing",
53
+ postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
54
+ authorEmail: "charlie@example.com",
55
+ },
56
+ ],
57
+ },
58
+ {
59
+ id: "6bcb2b74-9b2f-4b38-bdb5-77c2e63d9c10",
60
+ title: "Second post",
61
+ published: false,
62
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
63
+ comments: [],
64
+ },
65
+ ],
66
+ },
67
+ "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55": {
68
+ id: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
69
+ clerkId: "user_xyz789",
70
+ email: "dave@example.com",
71
+ createdAt: "2025-02-03T14:18:10.000Z",
72
+
73
+ teamToUsers: [
74
+ {
75
+ teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
76
+ userId: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
77
+ role: "admin",
78
+ team: {
79
+ id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
80
+ name: "Blue Sharks",
81
+ },
82
+ },
83
+ ],
84
+
85
+ posts: [],
86
+ },
87
+ });
88
+ });
89
+
90
+ test("Tests empty input.", () => {
91
+ type InputRow = {
92
+ user: {
93
+ clerk_id: string;
94
+ };
95
+ };
96
+
97
+ const input: InputRow[] = [];
98
+
99
+ expect(
100
+ relationMatcherRoot(input, {
101
+ base: "user",
102
+ id: "clerk_id",
103
+ }),
104
+ ).toStrictEqual({});
105
+ });
package/src/index.ts CHANGED
@@ -1,151 +1,160 @@
1
- import type { TInputBase } from "./types/generic-bases";
2
- import type { Output } from "./types/generic-less";
3
- import type { RelationMapRoot } from "./types/inputs";
4
- import type { RelationMapperReturnRoot } from "./types/return";
5
- import invertInput, { type InvertedInput } from "./utils/invertInput";
6
- import { getJoinFinalKey } from "./utils/keys";
7
- import { matcherFunc } from "./utils/matcher";
8
-
9
- export const relationMatcherRoot = <
10
- TInput extends TInputBase,
11
- TOutputRoot extends RelationMapRoot<TInput>,
12
- >(
13
- inputs: TInput[],
14
- output: TOutputRoot,
15
- ): Record<string, RelationMapperReturnRoot<TInput, TOutputRoot>> => {
16
- if (!inputs.length) return {};
17
-
18
- if (!Array.isArray(inputs)) {
19
- throw new Error("Input must be an array.");
20
- }
21
-
22
- const invertedInput = invertInput<TInput>(inputs);
23
-
24
- const baseItems = invertedInput[output.base].reduce<
25
- Record<string, TInput[TOutputRoot["base"]]>
26
- >((final, row) => {
27
- if (!row) {
28
- return final;
29
- }
30
-
31
- const relations = Object.entries(output).reduce<
32
- Record<string, unknown>
33
- >((final, [key, value]) => {
34
- if (typeof value === "object") {
35
- const finalKey = getJoinFinalKey(key);
36
-
37
- if (value.joinType === "array") {
38
- const baseObjArr = invertedInput[value.base]!.filter(
39
- matcherFunc(row, value as Output),
40
- ) as TInputBase[string][];
41
-
42
- final[finalKey] = Object.values(
43
- baseObjArr.reduce<
44
- Record<string, NonNullable<TInputBase[string]>>
45
- >((final, item) => {
46
- if (!item) return final;
47
-
48
- final[item[value.id as string] as string] = {
49
- ...item,
50
- ...relationMatcherJoiner(
51
- invertedInput,
52
- value as Output,
53
- item,
54
- ),
55
- };
56
-
57
- return final;
58
- }, {}),
59
- );
60
- } else {
61
- const item =
62
- (invertedInput[value.base]!.find(
63
- matcherFunc(row, value as Output),
64
- ) as TInputBase[string] | undefined) ?? null;
65
-
66
- if (item) {
67
- final[finalKey] = {
68
- ...item,
69
- ...relationMatcherJoiner(
70
- invertedInput,
71
- value as Output,
72
- item,
73
- ),
74
- };
75
- }
76
- }
77
- }
78
-
79
- return final;
80
- }, {}) as TInput[TOutputRoot["base"]];
81
-
82
- final[row[output.id as string] as string] = { ...row, ...relations };
83
-
84
- return final;
85
- }, {});
86
-
87
- return baseItems as unknown as Record<
88
- string,
89
- RelationMapperReturnRoot<TInput, TOutputRoot>
90
- >;
91
- };
92
-
93
- export const relationMatcher = relationMatcherRoot;
94
- export default relationMatcherRoot;
95
-
96
- const relationMatcherJoiner = (
97
- input: InvertedInput,
98
- output: Output,
99
- joiningFrom: Record<string, unknown>,
100
- ) => {
101
- return Object.entries(output).reduce<
102
- Record<string, TInputBase[string] | TInputBase[string][] | null>
103
- >((final, [key, value]) => {
104
- if (typeof value === "object") {
105
- const finalKey = key.replace(/^_/, "");
106
-
107
- const matcherFunc = (
108
- item: InvertedInput[keyof InvertedInput][number],
109
- ) => item?.[value.joinsTo] === joiningFrom[value.joinsFrom];
110
-
111
- if (value.joinType === "array") {
112
- const baseObjArr = input[value.base]!.filter(
113
- matcherFunc,
114
- ) as InvertedInput[number];
115
-
116
- final[finalKey] = Object.values(
117
- baseObjArr.reduce<
118
- Record<string, NonNullable<TInputBase[string]>>
119
- >((final, item) => {
120
- if (!item) return final;
121
-
122
- const propertyKey = item[value.id as string] as string;
123
-
124
- if (final[propertyKey]) return final;
125
-
126
- final[propertyKey] = {
127
- ...item,
128
- ...relationMatcherJoiner(input, value, item),
129
- };
130
-
131
- return final;
132
- }, {}),
133
- );
134
- } else {
135
- const item =
136
- (input[value.base]!.find(matcherFunc) as
137
- | InvertedInput[string][number]
138
- | undefined) ?? null;
139
-
140
- if (item) {
141
- final[finalKey] = {
142
- ...item,
143
- ...relationMatcherJoiner(input, value, item),
144
- };
145
- }
146
- }
147
- }
148
-
149
- return final;
150
- }, {});
151
- };
1
+ import type { TInputBase } from "./types/generic-bases";
2
+ import type { Output } from "./types/generic-less";
3
+ import type { RelationMapRoot } from "./types/inputs";
4
+ import type { RelationMapperReturnRoot } from "./types/return";
5
+ import invertInput, { type InvertedInput } from "./utils/invertInput";
6
+ import { getJoinFinalKey } from "./utils/keys";
7
+ import { matcherFunc } from "./utils/matcher";
8
+
9
+ export const relationMatcherRoot = <
10
+ TInput extends TInputBase,
11
+ TOutputRoot extends RelationMapRoot<TInput>,
12
+ >(
13
+ inputs: TInput[],
14
+ output: TOutputRoot,
15
+ ): Record<string, RelationMapperReturnRoot<TInput, TOutputRoot>> => {
16
+ if (!inputs.length) return {};
17
+
18
+ if (!Array.isArray(inputs)) {
19
+ throw new Error("Input must be an array.");
20
+ }
21
+
22
+ const invertedInput = invertInput<TInput>(inputs);
23
+
24
+ const baseItems = invertedInput[output.base].reduce<
25
+ Record<string, TInput[TOutputRoot["base"]]>
26
+ >((final, row) => {
27
+ if (!row) {
28
+ return final;
29
+ }
30
+
31
+ const relations = Object.entries(output).reduce<
32
+ Record<string, unknown>
33
+ >((final, [key, value]) => {
34
+ if (typeof value === "object") {
35
+ const finalKey = getJoinFinalKey(key);
36
+
37
+ if (value.joinType === "array") {
38
+ const baseObjArr = invertedInput[value.base]!.filter(
39
+ matcherFunc(row, value as Output),
40
+ ) as TInputBase[string][];
41
+
42
+ final[finalKey] = Object.values(
43
+ baseObjArr.reduce<
44
+ Record<string, NonNullable<TInputBase[string]>>
45
+ >((final, item) => {
46
+ if (!item) return final;
47
+
48
+ final[item[value.id as string] as string] = {
49
+ ...item,
50
+ ...relationMatcherJoiner(
51
+ invertedInput,
52
+ value as Output,
53
+ item,
54
+ ),
55
+ };
56
+
57
+ return final;
58
+ }, {}),
59
+ );
60
+ } else {
61
+ const item =
62
+ (invertedInput[value.base]!.find(
63
+ matcherFunc(row, value as Output),
64
+ ) as TInputBase[string] | undefined) ?? null;
65
+
66
+ if (item) {
67
+ final[finalKey] = {
68
+ ...item,
69
+ ...relationMatcherJoiner(
70
+ invertedInput,
71
+ value as Output,
72
+ item,
73
+ ),
74
+ };
75
+ }
76
+ }
77
+ }
78
+
79
+ return final;
80
+ }, {}) as TInput[TOutputRoot["base"]];
81
+
82
+ final[row[output.id as string] as string] = { ...row, ...relations };
83
+
84
+ return final;
85
+ }, {});
86
+
87
+ return baseItems as unknown as Record<
88
+ string,
89
+ RelationMapperReturnRoot<TInput, TOutputRoot>
90
+ >;
91
+ };
92
+
93
+ export const relationMatcher = relationMatcherRoot;
94
+ export default relationMatcherRoot;
95
+
96
+ const relationMatcherJoiner = (
97
+ input: InvertedInput,
98
+ output: Output,
99
+ joiningFrom: Record<string, unknown>,
100
+ ) => {
101
+ return Object.entries(output).reduce<
102
+ Record<string, TInputBase[string] | TInputBase[string][] | null>
103
+ >((final, [key, value]) => {
104
+ if (typeof value === "object") {
105
+ const finalKey = getJoinFinalKey(key);
106
+
107
+ const matcherFunc = (
108
+ item: InvertedInput[keyof InvertedInput][number],
109
+ ) => item?.[value.joinsTo] === joiningFrom[value.joinsFrom];
110
+
111
+ if (!input[value.base])
112
+ throw new Error(`Input is missing rows for ${value.base}.`);
113
+
114
+ if (value.joinType === "array") {
115
+ const baseObjArr = input[value.base]!.filter(
116
+ matcherFunc,
117
+ ) as InvertedInput[number];
118
+
119
+ final[finalKey] = Object.values(
120
+ baseObjArr.reduce<
121
+ Record<string, NonNullable<TInputBase[string]>>
122
+ >((final, item) => {
123
+ if (!item) return final;
124
+
125
+ const propertyKey = item[value.id as string] as string;
126
+
127
+ if (final[propertyKey]) return final;
128
+
129
+ final[propertyKey] = {
130
+ ...item,
131
+ ...relationMatcherJoiner(input, value, item),
132
+ };
133
+
134
+ return final;
135
+ }, {}),
136
+ );
137
+ } else {
138
+ const item =
139
+ (input[value.base]!.find(matcherFunc) as
140
+ | InvertedInput[string][number]
141
+ | undefined) ?? null;
142
+
143
+ if (item === null && value.isNullable === false) {
144
+ throw new Error(
145
+ "Value should exist as output schema defines it as nullable. Instead found null in array",
146
+ );
147
+ }
148
+
149
+ if (item) {
150
+ final[finalKey] = {
151
+ ...item,
152
+ ...relationMatcherJoiner(input, value, item),
153
+ };
154
+ }
155
+ }
156
+ }
157
+
158
+ return final;
159
+ }, {});
160
+ };
@@ -1,15 +1,15 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import { relationMatcherRoot } from "..";
4
- import { testData, testSchema } from "./test-data";
5
-
6
- const main = async () => {
7
- const result = relationMatcherRoot(testData, testSchema);
8
-
9
- await fs.writeFile(
10
- path.join(process.cwd(), "/result.json"),
11
- JSON.stringify(result, null, "\t"),
12
- );
13
- };
14
-
15
- void main();
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { relationMatcherRoot } from "..";
4
+ import { testData, testSchema } from "./test-data";
5
+
6
+ const main = async () => {
7
+ const result = relationMatcherRoot(testData, testSchema);
8
+
9
+ await fs.writeFile(
10
+ path.join(process.cwd(), "/result.json"),
11
+ JSON.stringify(result, null, "\t"),
12
+ );
13
+ };
14
+
15
+ void main();