relation-matcher 1.0.3

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 (50) hide show
  1. package/.prettierrc +4 -0
  2. package/.vscode/settings.json +3 -0
  3. package/.yarn/install-state.gz +0 -0
  4. package/.yarnrc.yml +1 -0
  5. package/dist/dist/jest.config.js +10 -0
  6. package/dist/dist/src/index.js +107 -0
  7. package/dist/dist/src/index.test.js +81 -0
  8. package/dist/dist/src/testing/file-output.js +9 -0
  9. package/dist/dist/src/testing/test-data.js +164 -0
  10. package/dist/dist/src/types/generic-bases.js +1 -0
  11. package/dist/dist/src/types/generic-less.js +1 -0
  12. package/dist/dist/src/types/inputs.js +1 -0
  13. package/dist/dist/src/types/return.js +1 -0
  14. package/dist/dist/src/types/typetest.js +6 -0
  15. package/dist/dist/src/types/utils.js +1 -0
  16. package/dist/dist/src/utils/invertInput.js +17 -0
  17. package/dist/dist/src/utils/keys.js +1 -0
  18. package/dist/dist/src/utils/matcher.js +1 -0
  19. package/dist/jest.config.js +10 -0
  20. package/dist/src/index.js +107 -0
  21. package/dist/src/index.test.js +81 -0
  22. package/dist/src/testing/file-output.js +9 -0
  23. package/dist/src/testing/test-data.js +164 -0
  24. package/dist/src/types/generic-bases.js +1 -0
  25. package/dist/src/types/generic-less.js +1 -0
  26. package/dist/src/types/inputs.js +1 -0
  27. package/dist/src/types/return.js +1 -0
  28. package/dist/src/types/typetest.js +6 -0
  29. package/dist/src/types/utils.js +1 -0
  30. package/dist/src/utils/invertInput.js +17 -0
  31. package/dist/src/utils/keys.js +1 -0
  32. package/dist/src/utils/matcher.js +1 -0
  33. package/dist/tsconfig.tsbuildinfo +1 -0
  34. package/jest.config.js +12 -0
  35. package/package.json +25 -0
  36. package/result.json +136 -0
  37. package/src/index.test.ts +88 -0
  38. package/src/index.ts +184 -0
  39. package/src/testing/file-output.ts +15 -0
  40. package/src/testing/test-data.ts +171 -0
  41. package/src/types/generic-bases.ts +9 -0
  42. package/src/types/generic-less.ts +14 -0
  43. package/src/types/inputs.ts +29 -0
  44. package/src/types/return.ts +37 -0
  45. package/src/types/typetest.ts +46 -0
  46. package/src/types/utils.ts +16 -0
  47. package/src/utils/invertInput.ts +28 -0
  48. package/src/utils/keys.ts +1 -0
  49. package/src/utils/matcher.ts +8 -0
  50. package/tsconfig.json +36 -0
package/jest.config.js ADDED
@@ -0,0 +1,12 @@
1
+ import { createDefaultPreset } from "ts-jest";
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ export default {
7
+ preset: "ts-jest",
8
+ testEnvironment: "node",
9
+ transform: {
10
+ ...tsJestTransformCfg,
11
+ },
12
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "relation-matcher",
3
+ "version": "1.0.3",
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
+ "private": false,
11
+ "scripts": {
12
+ "test": "jest",
13
+ "file-output": "tsx --watch src/file-output.ts",
14
+ "build": "tsc",
15
+ "build:publish": "yarn run build && npm publish"
16
+ },
17
+ "packageManager": "yarn@4.12.0",
18
+ "devDependencies": {
19
+ "@types/jest": "^30.0.0",
20
+ "@types/node": "^25.0.10",
21
+ "jest": "^30.2.0",
22
+ "ts-jest": "^29.4.6",
23
+ "typescript": "^5.9.3"
24
+ }
25
+ }
package/result.json ADDED
@@ -0,0 +1,136 @@
1
+ {
2
+ "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01": {
3
+ "id": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
4
+ "clerkId": "user_abc123",
5
+ "email": "alice@example.com",
6
+ "createdAt": "2025-01-12T09:41:22.000Z",
7
+ "teamToUsers": [
8
+ {
9
+ "teamId": "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
10
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
11
+ "role": "admin",
12
+ "team": {
13
+ "id": "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
14
+ "name": "Red Dragons",
15
+ "teamToUsers": [
16
+ {
17
+ "teamId": "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
18
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
19
+ "role": "admin",
20
+ "user": {
21
+ "id": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
22
+ "clerkId": "user_abc123",
23
+ "email": "alice@example.com",
24
+ "createdAt": "2025-01-12T09:41:22.000Z"
25
+ }
26
+ }
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
32
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
33
+ "role": "member",
34
+ "team": {
35
+ "id": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
36
+ "name": "Blue Sharks",
37
+ "teamToUsers": [
38
+ {
39
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
40
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
41
+ "role": "member",
42
+ "user": {
43
+ "id": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
44
+ "clerkId": "user_abc123",
45
+ "email": "alice@example.com",
46
+ "createdAt": "2025-01-12T09:41:22.000Z"
47
+ }
48
+ },
49
+ {
50
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
51
+ "userId": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
52
+ "role": "admin",
53
+ "user": {
54
+ "id": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
55
+ "clerkId": "user_xyz789",
56
+ "email": "dave@example.com",
57
+ "createdAt": "2025-02-03T14:18:10.000Z"
58
+ }
59
+ }
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ "posts": [
65
+ {
66
+ "id": "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
67
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
68
+ "title": "First post",
69
+ "published": true,
70
+ "comments": [
71
+ {
72
+ "id": "9d1a7c3b-2f6a-4f7c-bf4b-8f6e3c5d9e01",
73
+ "postId": "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
74
+ "body": "Nice post!",
75
+ "authorEmail": "bob@example.com"
76
+ },
77
+ {
78
+ "id": "e3c9b5a2-7f42-4b7e-9e3d-4a6f1d8b2c44",
79
+ "postId": "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
80
+ "body": "Thanks for sharing",
81
+ "authorEmail": "charlie@example.com"
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "id": "6bcb2b74-9b2f-4b38-bdb5-77c2e63d9c10",
87
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
88
+ "title": "Second post",
89
+ "published": false,
90
+ "comments": []
91
+ }
92
+ ]
93
+ },
94
+ "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55": {
95
+ "id": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
96
+ "clerkId": "user_xyz789",
97
+ "email": "dave@example.com",
98
+ "createdAt": "2025-02-03T14:18:10.000Z",
99
+ "teamToUsers": [
100
+ {
101
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
102
+ "userId": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
103
+ "role": "admin",
104
+ "team": {
105
+ "id": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
106
+ "name": "Blue Sharks",
107
+ "teamToUsers": [
108
+ {
109
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
110
+ "userId": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
111
+ "role": "member",
112
+ "user": {
113
+ "id": "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
114
+ "clerkId": "user_abc123",
115
+ "email": "alice@example.com",
116
+ "createdAt": "2025-01-12T09:41:22.000Z"
117
+ }
118
+ },
119
+ {
120
+ "teamId": "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
121
+ "userId": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
122
+ "role": "admin",
123
+ "user": {
124
+ "id": "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
125
+ "clerkId": "user_xyz789",
126
+ "email": "dave@example.com",
127
+ "createdAt": "2025-02-03T14:18:10.000Z"
128
+ }
129
+ }
130
+ ]
131
+ }
132
+ }
133
+ ],
134
+ "posts": []
135
+ }
136
+ }
@@ -0,0 +1,88 @@
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
+ });
package/src/index.ts ADDED
@@ -0,0 +1,184 @@
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
+ const invertedInput = invertInput<TInput>(inputs);
17
+
18
+ const baseItems = invertedInput[output.base].reduce<
19
+ Record<string, TInput[TOutputRoot["base"]]>
20
+ >((final, row) => {
21
+ if (!row) {
22
+ return final;
23
+ }
24
+ // if (final[row[output.id as string] as string]) {
25
+ // return final;
26
+ // }
27
+
28
+ const relations = Object.entries(output).reduce<
29
+ Record<string, unknown>
30
+ >((final, [key, value]) => {
31
+ // Filters out base parameters so value is only joins;
32
+ // value will be similar to { base: "team_to_user"; joinsTo: "team_id"; joinsFrom: "id"; joinType: "single"; }
33
+ if (typeof value === "object") {
34
+ const finalKey = getJoinFinalKey(key);
35
+
36
+ if (value.joinType === "array") {
37
+ const baseObjArr = invertedInput[value.base]!.filter(
38
+ matcherFunc(row, value as Output),
39
+ ) as TInputBase[string][];
40
+
41
+ final[finalKey] = Object.values(
42
+ baseObjArr.reduce<
43
+ Record<string, NonNullable<TInputBase[string]>>
44
+ >((final, item) => {
45
+ if (
46
+ !item
47
+ // final[item[value.id as string] as string]
48
+ )
49
+ return final;
50
+
51
+ // if (!item[value.joinsFrom as string]) {
52
+ console.log(item, value.joinsFrom);
53
+ // }
54
+
55
+ final[item[value.id as string] as string] = {
56
+ ...item,
57
+ ...relationMatcherJoiner(
58
+ invertedInput,
59
+ value as Output,
60
+ item,
61
+ ),
62
+ };
63
+
64
+ return final;
65
+ }, {}),
66
+ );
67
+ } else {
68
+ const item =
69
+ (invertedInput[value.base]!.find(
70
+ matcherFunc(row, value as Output),
71
+ ) as TInputBase[string] | undefined) ?? null;
72
+
73
+ if (item) {
74
+ final[finalKey] = {
75
+ ...item,
76
+ ...relationMatcherJoiner(
77
+ invertedInput,
78
+ value as Output,
79
+ item,
80
+ ),
81
+ };
82
+ }
83
+ }
84
+
85
+ // if (value.joinType === 'array')
86
+
87
+ // final[finalKey] = relationMatcherJoiner(
88
+ // invertedInput,
89
+ // value as Output,
90
+ // row[value.joinsFrom as string] as string,
91
+ // );
92
+ }
93
+
94
+ return final;
95
+ }, {}) as TInput[TOutputRoot["base"]];
96
+
97
+ final[row[output.id as string] as string] = { ...row, ...relations };
98
+
99
+ return final;
100
+ }, {});
101
+
102
+ return baseItems as Record<
103
+ string,
104
+ RelationMapperReturnRoot<TInput, TOutputRoot>
105
+ >;
106
+ };
107
+
108
+ export const relationMatcher = relationMatcherRoot;
109
+ export default relationMatcherRoot;
110
+
111
+ const relationMatcherJoiner = (
112
+ input: InvertedInput,
113
+ output: Output,
114
+ joiningFrom: Record<string, unknown>,
115
+ ) => {
116
+ return Object.entries(output).reduce<
117
+ Record<string, TInputBase[string] | TInputBase[string][] | null>
118
+ >((final, [key, value]) => {
119
+ if (typeof value === "object") {
120
+ // console.log(value.base);
121
+ const finalKey = key.replace(/^_/, "");
122
+
123
+ const matcherFunc = (
124
+ item: InvertedInput[keyof InvertedInput][number],
125
+ ) => item?.[value.joinsTo] === joiningFrom[value.joinsFrom];
126
+
127
+ if (value.joinType === "array") {
128
+ const baseObjArr = input[value.base]!.filter(
129
+ matcherFunc,
130
+ ) as InvertedInput[number];
131
+
132
+ // console.log("BaseObjArr:", baseObjArr);
133
+ // console.log(
134
+ // "input[value.base]:",
135
+ // input[value.base]?.[0]?.[value.joinsTo],
136
+ // );
137
+ // console.log("joiningId", joiningId);
138
+
139
+ final[finalKey] = Object.values(
140
+ baseObjArr.reduce<
141
+ Record<string, NonNullable<TInputBase[string]>>
142
+ >((final, item) => {
143
+ if (!item) return final;
144
+
145
+ const propertyKey = item[value.id as string] as string;
146
+
147
+ if (final[propertyKey]) return final;
148
+
149
+ final[propertyKey] = {
150
+ ...item,
151
+ ...relationMatcherJoiner(input, value, item),
152
+ };
153
+
154
+ return final;
155
+ }, {}),
156
+ );
157
+
158
+ // baseObjArr.map(item => (item ? {
159
+ // ...item,
160
+ // ...relationMatcherJoiner(input, value, item[value.joinsFrom as keyof TInputBase])
161
+ // } : null))
162
+ } else {
163
+ const item =
164
+ (input[value.base]!.find(matcherFunc) as
165
+ | InvertedInput[string][number]
166
+ | undefined) ?? null;
167
+
168
+ // console.log("item:", item);
169
+ // console.log("input[value.base]:", input[value.base]);
170
+ // console.log("joiningId:", joiningId);
171
+ // console.log("value:", value);
172
+
173
+ if (item) {
174
+ final[finalKey] = {
175
+ ...item,
176
+ ...relationMatcherJoiner(input, value, item),
177
+ };
178
+ }
179
+ }
180
+ }
181
+
182
+ return final;
183
+ }, {});
184
+ };
@@ -0,0 +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();
@@ -0,0 +1,171 @@
1
+ import type { RelationMapRoot } from "~/types/inputs";
2
+
3
+ export const testData = [
4
+ {
5
+ users: {
6
+ id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
7
+ clerkId: "user_abc123",
8
+ email: "alice@example.com",
9
+ createdAt: "2025-01-12T09:41:22.000Z",
10
+ },
11
+ teamToUser: {
12
+ teamId: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
13
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
14
+ role: "admin",
15
+ },
16
+ teams: {
17
+ id: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
18
+ name: "Red Dragons",
19
+ },
20
+ posts: {
21
+ id: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
22
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
23
+ title: "First post",
24
+ published: true,
25
+ },
26
+ comments: {
27
+ id: "9d1a7c3b-2f6a-4f7c-bf4b-8f6e3c5d9e01",
28
+ postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
29
+ body: "Nice post!",
30
+ authorEmail: "bob@example.com",
31
+ },
32
+ },
33
+ {
34
+ users: {
35
+ id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
36
+ clerkId: "user_abc123",
37
+ email: "alice@example.com",
38
+ createdAt: "2025-01-12T09:41:22.000Z",
39
+ },
40
+ teamToUser: {
41
+ teamId: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
42
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
43
+ role: "admin",
44
+ },
45
+ teams: {
46
+ id: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
47
+ name: "Red Dragons",
48
+ },
49
+ posts: {
50
+ id: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
51
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
52
+ title: "First post",
53
+ published: true,
54
+ },
55
+ comments: {
56
+ id: "e3c9b5a2-7f42-4b7e-9e3d-4a6f1d8b2c44",
57
+ postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
58
+ body: "Thanks for sharing",
59
+ authorEmail: "charlie@example.com",
60
+ },
61
+ },
62
+ {
63
+ users: {
64
+ id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
65
+ clerkId: "user_abc123",
66
+ email: "alice@example.com",
67
+ createdAt: "2025-01-12T09:41:22.000Z",
68
+ },
69
+ teamToUser: {
70
+ teamId: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
71
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
72
+ role: "admin",
73
+ },
74
+ teams: {
75
+ id: "a2e5a3de-6d14-4e9b-9c9f-3cbb2cdb8a10",
76
+ name: "Red Dragons",
77
+ },
78
+ posts: {
79
+ id: "6bcb2b74-9b2f-4b38-bdb5-77c2e63d9c10",
80
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
81
+ title: "Second post",
82
+ published: false,
83
+ },
84
+ comments: null,
85
+ },
86
+ {
87
+ users: {
88
+ id: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
89
+ clerkId: "user_abc123",
90
+ email: "alice@example.com",
91
+ createdAt: "2025-01-12T09:41:22.000Z",
92
+ },
93
+ teamToUser: {
94
+ teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
95
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
96
+ role: "member",
97
+ },
98
+ teams: {
99
+ id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
100
+ name: "Blue Sharks",
101
+ },
102
+ posts: {
103
+ id: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
104
+ userId: "c7a2c1c8-9f4c-4f89-9d72-6b5a2f0c1e01",
105
+ title: "First post",
106
+ published: true,
107
+ },
108
+ comments: {
109
+ id: "9d1a7c3b-2f6a-4f7c-bf4b-8f6e3c5d9e01",
110
+ postId: "f13d8f22-0b61-4d6a-8b1e-5b6b3d0c8a21",
111
+ body: "Nice post!",
112
+ authorEmail: "bob@example.com",
113
+ },
114
+ },
115
+ {
116
+ users: {
117
+ id: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
118
+ clerkId: "user_xyz789",
119
+ email: "dave@example.com",
120
+ createdAt: "2025-02-03T14:18:10.000Z",
121
+ },
122
+ teamToUser: {
123
+ teamId: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
124
+ userId: "f44a8c17-3c6d-4e38-9f61-0a9f2b1c8d55",
125
+ role: "admin",
126
+ },
127
+ teams: {
128
+ id: "d91f42a6-8cbb-4e63-9b5c-8d1b4f2a7e77",
129
+ name: "Blue Sharks",
130
+ },
131
+ posts: null,
132
+ comments: null,
133
+ },
134
+ ];
135
+
136
+ export const testSchema: RelationMapRoot<(typeof testData)[number]> = {
137
+ base: "users",
138
+ id: "id",
139
+
140
+ _teamToUsers: {
141
+ base: "teamToUser",
142
+ id: "teamId",
143
+ joinsFrom: "id",
144
+ joinsTo: "userId",
145
+ joinType: "array",
146
+
147
+ _team: {
148
+ base: "teams",
149
+ id: "id",
150
+ joinsFrom: "teamId",
151
+ joinsTo: "id",
152
+ joinType: "single",
153
+ },
154
+ },
155
+
156
+ _posts: {
157
+ base: "posts",
158
+ id: "id",
159
+ joinsFrom: "id",
160
+ joinsTo: "userId",
161
+ joinType: "array",
162
+
163
+ _comments: {
164
+ base: "comments",
165
+ id: "id",
166
+ joinsFrom: "id",
167
+ joinsTo: "postId",
168
+ joinType: "array",
169
+ },
170
+ },
171
+ };
@@ -0,0 +1,9 @@
1
+ import type { RelationMapBase, RelationMapJoiner } from "./inputs";
2
+
3
+ export type TInputBase = Record<string, Record<string, unknown> | null>;
4
+ export type TJoinedFromBase = TInputBase[keyof TInputBase];
5
+ export type TOutputBase<
6
+ TInput extends TInputBase,
7
+ TJoinedFrom extends TJoinedFromBase,
8
+ > = RelationMapBase<TInput, TJoinedFrom> &
9
+ RelationMapJoiner<TInput, TJoinedFrom>;
@@ -0,0 +1,14 @@
1
+ import type { TInputBase } from "./generic-bases";
2
+
3
+ export type Output<TJoinType extends "single" | "array" = "single" | "array"> =
4
+ OutputJoins & {
5
+ base: keyof TInputBase;
6
+ id: string;
7
+ joinsTo: string;
8
+ joinsFrom: string;
9
+ joinType: TJoinType;
10
+ };
11
+
12
+ export type OutputJoins = {
13
+ [k: `_${string}`]: Output;
14
+ };
@@ -0,0 +1,29 @@
1
+ import type { TInputBase, TJoinedFromBase } from "./generic-bases";
2
+
3
+ export type RelationMapRoot<TInput extends TInputBase> = {
4
+ [k in keyof TInput]: {
5
+ base: k;
6
+ id: keyof TInput[k];
7
+ } & RelationMapJoiner<TInput, TInput[k]>;
8
+ }[keyof TInput];
9
+
10
+ export type RelationMapBase<
11
+ TInput extends TInputBase,
12
+ TJoinedFrom extends TJoinedFromBase,
13
+ > = {
14
+ [k in keyof TInput]: {
15
+ base: k;
16
+ id: keyof TInput[k];
17
+ joinsTo: keyof TInput[k];
18
+ joinsFrom: keyof TJoinedFrom;
19
+ joinType: "single" | "array";
20
+ } & RelationMapJoiner<TInput, TInput[k]>;
21
+ }[keyof TInput];
22
+
23
+ export type RelationMapJoiner<
24
+ TInput extends TInputBase,
25
+ TJoinedFrom extends TJoinedFromBase,
26
+ > = {
27
+ [k: `_${string}`]: RelationMapJoiner<TInput, TJoinedFrom> &
28
+ RelationMapBase<TInput, TJoinedFrom>;
29
+ };