relation-matcher 1.1.0 → 1.1.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "relation-matcher",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A utility to convert table data (such as out of a SQL query) into structured JSON.",
5
5
  "license": "ISC",
6
6
  "author": "",
package/src/index.ts CHANGED
@@ -1,18 +1,21 @@
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";
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";
5
6
  import invertInput, { type InvertedInput } from "./utils/invertInput";
7
+ import { assertIsNullable } from "./utils/isNullable";
8
+ import { arrayJoinBase, singleJoinBase } from "./utils/joins";
6
9
  import { getJoinFinalKey } from "./utils/keys";
7
- import { matcherFunc } from "./utils/matcher";
10
+ import { createMatcherFunc } from "./utils/matcher";
8
11
 
9
12
  export const relationMatcherRoot = <
10
13
  TInput extends TInputBase,
11
- TOutputRoot extends RelationMapRoot<TInput>,
14
+ TOutputRoot extends Root<TInput>,
12
15
  >(
13
16
  inputs: TInput[],
14
17
  output: TOutputRoot,
15
- ): Record<string, RelationMapperReturnRoot<TInput, TOutputRoot>> => {
18
+ ): SRecord<ReturnRoot<TInput, TOutputRoot>> => {
16
19
  if (!inputs.length) return {};
17
20
 
18
21
  if (!Array.isArray(inputs)) {
@@ -21,140 +24,101 @@ export const relationMatcherRoot = <
21
24
 
22
25
  const invertedInput = invertInput<TInput>(inputs);
23
26
 
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
- }
27
+ type BaseItems = SRecord<TInput[TOutputRoot["base"]]>;
28
+ const baseItems = invertedInput[output.base].reduce<BaseItems>(
29
+ (final, inputRow) => {
30
+ if (!inputRow) {
31
+ return final;
77
32
  }
78
33
 
79
- return final;
80
- }, {}) as TInput[TOutputRoot["base"]];
34
+ const relations = joiner(
35
+ invertedInput,
36
+ output as OutputRoot,
37
+ inputRow,
38
+ );
81
39
 
82
- final[row[output.id as string] as string] = { ...row, ...relations };
40
+ const rowId = inputRow[output.id as string];
83
41
 
84
- return final;
85
- }, {});
42
+ final[rowId as string] = {
43
+ ...inputRow,
44
+ ...relations,
45
+ };
46
+
47
+ return final;
48
+ },
49
+ {},
50
+ );
86
51
 
87
52
  return baseItems as unknown as Record<
88
53
  string,
89
- RelationMapperReturnRoot<TInput, TOutputRoot>
54
+ ReturnRoot<TInput, TOutputRoot>
90
55
  >;
91
56
  };
92
57
 
93
58
  export const relationMatcher = relationMatcherRoot;
94
59
  export default relationMatcherRoot;
95
60
 
96
- const relationMatcherJoiner = (
61
+ const joiner = (
97
62
  input: InvertedInput,
98
- output: Output,
99
- joiningFrom: Record<string, unknown>,
63
+ output: Output | OutputRoot,
64
+ joiningFrom: SRecord<unknown>,
100
65
  ) => {
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);
66
+ type Return = SRecord<MaybeArray<MaybeNull<TInputBaseJoin>>>;
67
+ return Object.entries(output).reduce<Return>((final, [key, value]) => {
68
+ if (typeof value !== "object") return final;
106
69
 
107
- const matcherFunc = (
108
- item: InvertedInput[keyof InvertedInput][number],
109
- ) => item?.[value.joinsTo] === joiningFrom[value.joinsFrom];
70
+ const finalKey = getJoinFinalKey(key);
71
+ const matcherFunc = createMatcherFunc(joiningFrom, value);
110
72
 
111
- if (!input[value.base])
112
- throw new Error(`Input is missing rows for ${value.base}.`);
73
+ const joinType = input[value.base];
113
74
 
114
- if (value.joinType === "array") {
115
- const baseObjArr = input[value.base]!.filter(
116
- matcherFunc,
117
- ) as InvertedInput[number];
75
+ if (!joinType)
76
+ throw new Error(`Input is missing rows for ${value.base}.`);
118
77
 
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;
78
+ switch (value.joinType) {
79
+ case "array": {
80
+ const baseObjArr = arrayJoinBase(joinType, matcherFunc);
126
81
 
127
- if (final[propertyKey]) return final;
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;
128
86
 
129
- final[propertyKey] = {
87
+ final[propertyKey] ??= {
130
88
  ...item,
131
- ...relationMatcherJoiner(input, value, item),
89
+ ...joiner(input, value, item),
132
90
  };
133
91
 
134
92
  return final;
135
93
  }, {}),
136
94
  );
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
- }
95
+
96
+ break;
97
+ }
98
+ case "single": {
99
+ const item = singleJoinBase(joinType, matcherFunc);
100
+
101
+ assertIsNullable(item, value.isNullable, output, value.base);
148
102
 
149
103
  if (item) {
150
104
  final[finalKey] = {
151
105
  ...item,
152
- ...relationMatcherJoiner(input, value, item),
106
+ ...joiner(input, value, item),
153
107
  };
154
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
+ );
155
117
  }
156
118
  }
157
119
 
158
120
  return final;
159
121
  }, {});
160
122
  };
123
+
124
+ export { joiner as relationMatcherJoiner };
@@ -1,6 +1,7 @@
1
1
  import type { RelationMapBase, RelationMapJoiner } from "./inputs";
2
2
 
3
- export type TInputBase = Record<string, Record<string, unknown> | null>;
3
+ export type TInputBaseJoin = Record<string, unknown>;
4
+ export type TInputBase = Record<string, TInputBaseJoin | null>;
4
5
  export type TJoinedFromBase = TInputBase[keyof TInputBase];
5
6
  export type TOutputBase<
6
7
  TInput extends TInputBase,
@@ -1,14 +1,19 @@
1
1
  import type { TInputBase } from "./generic-bases";
2
2
 
3
+ export type OutputBase<
4
+ TJoinType extends "single" | "array" = "single" | "array",
5
+ > = {
6
+ base: string;
7
+ id: string;
8
+ joinsTo: string;
9
+ joinsFrom: string;
10
+ joinType: TJoinType;
11
+ isNullable?: false;
12
+ };
13
+
14
+ export type OutputRoot = OutputJoins & { base: string; id: string };
3
15
  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
- isNullable?: false;
11
- };
16
+ OutputJoins & OutputBase<TJoinType>;
12
17
 
13
18
  export type OutputJoins = {
14
19
  [k: `_${string}`]: Output;
@@ -26,3 +26,8 @@ export type IsNullIfNull<
26
26
  > = TIsNull extends infer T ? (T & TReturn) | null : TIsNull & TReturn;
27
27
 
28
28
  export type ExtendsNull<T> = null extends T ? true : false;
29
+
30
+ export type MaybeNull<T> = T | null;
31
+ export type MaybeArray<T> = T | T[];
32
+
33
+ export type SRecord<T> = Record<string, T>;
@@ -1,8 +1,10 @@
1
1
  import type { TInputBase } from "~/types/generic-bases";
2
2
 
3
+ export type InvertedInputRow<TInput extends TInputBase = TInputBase> =
4
+ TInput[keyof TInput][];
3
5
  export type InvertedInput<TInput extends TInputBase = TInputBase> = Record<
4
6
  keyof TInput,
5
- TInput[keyof TInput][]
7
+ InvertedInputRow<TInput>
6
8
  >;
7
9
 
8
10
  const invertInput = <TInput extends TInputBase>(
@@ -0,0 +1,15 @@
1
+ import type { Output, OutputRoot } from "~/types";
2
+
3
+ /** Asserts that if `isNullable` is false then item is not null. */
4
+ export const assertIsNullable = (
5
+ item: Record<string, unknown> | null,
6
+ isNullable: false | undefined,
7
+ output: Output | OutputRoot,
8
+ base: string,
9
+ ) => {
10
+ if (isNullable === false && item === null) {
11
+ throw new Error(
12
+ `Value should exist as output schema defines it as non-nullable. Instead found null in array. Found when joining ${base} onto ${output.base}.`,
13
+ );
14
+ }
15
+ };
@@ -0,0 +1,22 @@
1
+ import type { OutputBase, TInputBase } from "~/types";
2
+ import type { InvertedInput, InvertedInputRow } from "./invertInput";
3
+ import { type MatcherFunc } from "./matcher";
4
+
5
+ export const singleJoinBase = <TValue extends OutputBase<"single">>(
6
+ input: InvertedInputRow,
7
+ matcherFunc: MatcherFunc,
8
+ ): Record<string, unknown> | null => {
9
+ const item =
10
+ (input.find(matcherFunc) as TInputBase[string] | undefined) ?? null;
11
+
12
+ return item;
13
+ };
14
+
15
+ export const arrayJoinBase = <TValue extends OutputBase<"single">>(
16
+ input: InvertedInputRow,
17
+ matcherFunc: MatcherFunc,
18
+ ): Record<string, unknown>[] => {
19
+ const item = input.filter(matcherFunc);
20
+
21
+ return item;
22
+ };
@@ -1,8 +1,16 @@
1
- import type { TInputBase } from "~/types/generic-bases";
1
+ import type { TInputBase, TInputBaseJoin } from "~/types/generic-bases";
2
2
  import type { InvertedInput } from "./invertInput";
3
3
  import type { Output } from "~/types/generic-less";
4
4
 
5
- export const matcherFunc =
6
- <TInput extends TInputBase>(row: TInput[keyof TInput], value: Output) =>
7
- (item: InvertedInput[keyof InvertedInput][number]) =>
5
+ export type MatcherFunc = (
6
+ item: TInputBaseJoin | null,
7
+ ) => item is TInputBaseJoin;
8
+
9
+ export const createMatcherFunc =
10
+ <TInput extends TInputBase>(
11
+ row: TInput[keyof TInput],
12
+ value: Output,
13
+ ): MatcherFunc =>
14
+ (item): item is TInputBaseJoin =>
15
+ !!item &&
8
16
  row?.[value.joinsFrom as string] === item?.[value.joinsTo as string];