relation-matcher 1.0.13 → 1.1.1

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.
Files changed (40) hide show
  1. package/.vscode/settings.json +4 -4
  2. package/.yarn/install-state.gz +0 -0
  3. package/.yarn/releases/yarn-4.13.0.cjs +940 -940
  4. package/.yarnrc.yml +3 -3
  5. package/README.md +185 -150
  6. package/dist/src/index.d.ts +10 -5
  7. package/dist/src/index.js +35 -46
  8. package/dist/src/types/generic-bases.d.ts +2 -1
  9. package/dist/src/types/generic-less.d.ts +8 -3
  10. package/dist/src/types/inputs.d.ts +14 -7
  11. package/dist/src/types/return.d.ts +5 -2
  12. package/dist/src/types/typetest.d.ts +1 -0
  13. package/dist/src/types/utils.d.ts +3 -0
  14. package/dist/src/utils/invertInput.d.ts +2 -1
  15. package/dist/src/utils/invertInput.js +4 -9
  16. package/dist/src/utils/isNullable.d.ts +2 -0
  17. package/dist/src/utils/isNullable.js +6 -0
  18. package/dist/src/utils/joins.d.ts +5 -0
  19. package/dist/src/utils/joins.js +9 -0
  20. package/dist/src/utils/keys.js +6 -1
  21. package/dist/src/utils/matcher.d.ts +3 -3
  22. package/dist/src/utils/matcher.js +2 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +36 -36
  25. package/src/index.test.ts +105 -105
  26. package/src/index.ts +124 -151
  27. package/src/testing/file-output.ts +15 -15
  28. package/src/testing/test-data.ts +172 -172
  29. package/src/types/generic-bases.ts +10 -9
  30. package/src/types/generic-less.ts +20 -14
  31. package/src/types/index.ts +5 -5
  32. package/src/types/inputs.ts +36 -30
  33. package/src/types/return.ts +65 -67
  34. package/src/types/typetest.ts +87 -84
  35. package/src/types/utils.ts +33 -28
  36. package/src/utils/invertInput.ts +27 -28
  37. package/src/utils/isNullable.ts +11 -0
  38. package/src/utils/joins.ts +22 -0
  39. package/src/utils/keys.ts +7 -1
  40. package/src/utils/matcher.ts +16 -8
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "relation-matcher",
3
- "version": "1.0.13",
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.1",
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,124 @@
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 { MaybeArray, MaybeNull, SRecord } from "./types";
2
+ import type { TInputBase, TInputBaseJoin } from "./types/generic-bases";
3
+ import type { Output, OutputRoot } from "./types/generic-less";
4
+ import type { RelationMapRoot as Root } from "./types/inputs";
5
+ import type { RelationMapperReturnRoot as ReturnRoot } from "./types/return";
6
+ import invertInput, { type InvertedInput } from "./utils/invertInput";
7
+ import { assertIsNullable } from "./utils/isNullable";
8
+ import { arrayJoinBase, singleJoinBase } from "./utils/joins";
9
+ import { getJoinFinalKey } from "./utils/keys";
10
+ import { createMatcherFunc } from "./utils/matcher";
11
+
12
+ export const relationMatcherRoot = <
13
+ TInput extends TInputBase,
14
+ TOutputRoot extends Root<TInput>,
15
+ >(
16
+ inputs: TInput[],
17
+ output: TOutputRoot,
18
+ ): SRecord<ReturnRoot<TInput, TOutputRoot>> => {
19
+ if (!inputs.length) return {};
20
+
21
+ if (!Array.isArray(inputs)) {
22
+ throw new Error("Input must be an array.");
23
+ }
24
+
25
+ const invertedInput = invertInput<TInput>(inputs);
26
+
27
+ type BaseItems = SRecord<TInput[TOutputRoot["base"]]>;
28
+ const baseItems = invertedInput[output.base].reduce<BaseItems>(
29
+ (final, inputRow) => {
30
+ if (!inputRow) {
31
+ return final;
32
+ }
33
+
34
+ const relations = joiner(
35
+ invertedInput,
36
+ output as OutputRoot,
37
+ inputRow,
38
+ );
39
+
40
+ const rowId = inputRow[output.id as string];
41
+
42
+ final[rowId as string] = {
43
+ ...inputRow,
44
+ ...relations,
45
+ };
46
+
47
+ return final;
48
+ },
49
+ {},
50
+ );
51
+
52
+ return baseItems as unknown as Record<
53
+ string,
54
+ ReturnRoot<TInput, TOutputRoot>
55
+ >;
56
+ };
57
+
58
+ export const relationMatcher = relationMatcherRoot;
59
+ export default relationMatcherRoot;
60
+
61
+ const joiner = (
62
+ input: InvertedInput,
63
+ output: Output | OutputRoot,
64
+ joiningFrom: SRecord<unknown>,
65
+ ) => {
66
+ type Return = SRecord<MaybeArray<MaybeNull<TInputBaseJoin>>>;
67
+ return Object.entries(output).reduce<Return>((final, [key, value]) => {
68
+ if (typeof value !== "object") return final;
69
+
70
+ const finalKey = getJoinFinalKey(key);
71
+ const matcherFunc = createMatcherFunc(joiningFrom, value);
72
+
73
+ const joinType = input[value.base];
74
+
75
+ if (!joinType)
76
+ throw new Error(`Input is missing rows for ${value.base}.`);
77
+
78
+ switch (value.joinType) {
79
+ case "array": {
80
+ const baseObjArr = arrayJoinBase(joinType, matcherFunc);
81
+
82
+ type FinalRow = SRecord<NonNullable<TInputBase[string]>>;
83
+ final[finalKey] = Object.values(
84
+ baseObjArr.reduce<FinalRow>((final, item) => {
85
+ const propertyKey = item[value.id] as string;
86
+
87
+ final[propertyKey] ??= {
88
+ ...item,
89
+ ...joiner(input, value, item),
90
+ };
91
+
92
+ return final;
93
+ }, {}),
94
+ );
95
+
96
+ break;
97
+ }
98
+ case "single": {
99
+ const item = singleJoinBase(joinType, matcherFunc);
100
+
101
+ assertIsNullable(item, value.isNullable);
102
+
103
+ if (item) {
104
+ final[finalKey] = {
105
+ ...item,
106
+ ...joiner(input, value, item),
107
+ };
108
+ }
109
+
110
+ break;
111
+ }
112
+ default: {
113
+ value.joinType satisfies never;
114
+ throw new Error(
115
+ `Unhandled join type. Relation matcher can not handle ${value.joinType}.`,
116
+ );
117
+ }
118
+ }
119
+
120
+ return final;
121
+ }, {});
122
+ };
123
+
124
+ export { joiner as relationMatcherJoiner };
@@ -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();