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/.yarnrc.yml CHANGED
@@ -1,3 +1,3 @@
1
- nodeLinker: node-modules
2
-
3
- yarnPath: .yarn/releases/yarn-4.13.0.cjs
1
+ nodeLinker: node-modules
2
+
3
+ yarnPath: .yarn/releases/yarn-4.13.0.cjs
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # Relation Matcher
2
+
3
+ This is a utility to convert unstructured data from a database into structured relational json data.
4
+
5
+ ## Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [Data](#data)
10
+ - [Schema](#schema)
11
+ - [Output](#output)
12
+ - [Changelog](#changelog)
13
+
14
+ ## Installation
15
+
16
+ | | Installation Command |
17
+ | ---- | --------------------------- |
18
+ | npm | `npm i relation-matcher` |
19
+ | yarn | `yarn add relation-matcher` |
20
+ | pnpm | `pnpm add relation-matcher` |
21
+
22
+ ## Usage
23
+
24
+ To use the relation matcher you feed in your `data` from your database and a `schema`. For example with drizzle:
25
+
26
+ ```ts
27
+ // import db from "~/db/client";
28
+ // ...otherImports
29
+ import relationMatcher from "relation-matcher";
30
+
31
+ /* Get your data from the database */
32
+ const dbData = await db.select().fro...
33
+
34
+ const data /* Output */ = relationMatcher(
35
+ dbData /* Data */,
36
+ {...} /* Schema */
37
+ );
38
+
39
+ return data;
40
+ ```
41
+
42
+ ### `data`
43
+
44
+ The `data`'s shape should extend:
45
+
46
+ ```ts
47
+ Array<Record<string, Record<string, unknown> | null>>;
48
+ ```
49
+
50
+ #### Example
51
+
52
+ The data could have the following type:
53
+
54
+ ```ts
55
+ {
56
+ user: {
57
+ id: string;
58
+ email: string;
59
+ password: string;
60
+ ...otherColumns
61
+ };
62
+
63
+ post: {
64
+ id: string;
65
+ title: string;
66
+ content: string;
67
+
68
+ author_id: string; // < Same as user.id
69
+ post_category_id: string; // < Same as post_category.id
70
+ };
71
+
72
+ post_category: {
73
+ id: string;
74
+ name: string;
75
+ } | null;
76
+ }[]
77
+ ```
78
+
79
+ ### `schema`
80
+
81
+ The `schema`'s shape should be:
82
+
83
+ ```ts
84
+ {
85
+ base: "name of table",
86
+ id: "distinct column in above table",
87
+
88
+ _yourJoinedTableName: {
89
+ base: "name of table to join",
90
+ id: "distinct column of above table",
91
+ joinsFrom: "column of parent table",
92
+ joinsTo: "column of current table",
93
+ joinType: "single" | "array",
94
+ isNullable?: false | undefined, // No-op on array join types.
95
+ },
96
+ }
97
+ ```
98
+
99
+ > [!NOTE]
100
+ > Keys for joins start with an underscore; the final property key will have the leading underscore removed.
101
+
102
+ #### Example
103
+
104
+ With the above `data`, the `schema`'s shape could be:
105
+
106
+ ```ts
107
+ {
108
+ base: "user",
109
+ id: "id",
110
+
111
+ _posts: {
112
+ base: "post",
113
+ id: "id",
114
+ joinsFrom: "id",
115
+ joinsTo: "author_id",
116
+ joinType: "array",
117
+
118
+ _postCategory: {
119
+ base: "post_category",
120
+ id: "id",
121
+ joinsFrom: "post_category_id",
122
+ joinsTo: "id",
123
+ joinType: "single",
124
+ isNullable: false,
125
+ },
126
+ },
127
+ }
128
+ ```
129
+
130
+ ### `output`
131
+
132
+ The outputted data will take the shape of your schema.
133
+
134
+ #### Example
135
+
136
+ For the `data` and `schema` provided above, the `output` will have the following type:
137
+
138
+ ```ts
139
+ Record<
140
+ string, // The id specified in the root of your schema.
141
+ {
142
+ id: string;
143
+ email: string;
144
+ password: string;
145
+ ...otherColumns;
146
+
147
+ posts: Array<
148
+ {
149
+ id: string;
150
+ title: string;
151
+ content: string;
152
+
153
+ author_id: string;
154
+ post_category_id: string;
155
+
156
+ postCategory: {
157
+ id: string;
158
+ name: string;
159
+ } // | null;
160
+ // Note this is not nullable as the schema defined it as such.
161
+ }
162
+ >;
163
+ }
164
+ >;
165
+ ```
166
+
167
+ ## Changelog
168
+
169
+ ### 1.1.0
170
+
171
+ - Added ability to mark join as `isNullable: false`. This allows for manual marking of single joins as `NonNullable`.
172
+
173
+ Will throw `Error` if value is not found.
174
+
175
+ ### 1.0.13
176
+
177
+ - Added readme.
178
+
179
+ ### 1.0.12
180
+
181
+ - Fixed type issue where return type would be repeated union of expected `output`.
package/dist/src/index.js CHANGED
@@ -49,8 +49,10 @@ export default relationMatcherRoot;
49
49
  const relationMatcherJoiner = (input, output, joiningFrom) => {
50
50
  return Object.entries(output).reduce((final, [key, value]) => {
51
51
  if (typeof value === "object") {
52
- const finalKey = key.replace(/^_/, "");
52
+ const finalKey = getJoinFinalKey(key);
53
53
  const matcherFunc = (item) => item?.[value.joinsTo] === joiningFrom[value.joinsFrom];
54
+ if (!input[value.base])
55
+ throw new Error(`Input is missing rows for ${value.base}.`);
54
56
  if (value.joinType === "array") {
55
57
  const baseObjArr = input[value.base].filter(matcherFunc);
56
58
  final[finalKey] = Object.values(baseObjArr.reduce((final, item) => {
@@ -68,6 +70,9 @@ const relationMatcherJoiner = (input, output, joiningFrom) => {
68
70
  }
69
71
  else {
70
72
  const item = input[value.base].find(matcherFunc) ?? null;
73
+ if (item === null && value.isNullable === false) {
74
+ throw new Error("Value should exist as output schema defines it as nullable. Instead found null in array");
75
+ }
71
76
  if (item) {
72
77
  final[finalKey] = {
73
78
  ...item,
@@ -5,6 +5,7 @@ export type Output<TJoinType extends "single" | "array" = "single" | "array"> =
5
5
  joinsTo: string;
6
6
  joinsFrom: string;
7
7
  joinType: TJoinType;
8
+ isNullable?: false;
8
9
  };
9
10
  export type OutputJoins = {
10
11
  [k: `_${string}`]: Output;
@@ -5,15 +5,22 @@ export type RelationMapRoot<TInput extends TInputBase> = {
5
5
  id: keyof NonNullable<TInput[k]>;
6
6
  } & RelationMapJoiner<TInput, NonNullable<TInput[k]>>;
7
7
  }[keyof TInput];
8
+ export type IsNullable = {
9
+ false: false;
10
+ undefined: undefined;
11
+ };
8
12
  export type RelationMapBase<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase> = {
9
13
  [k in keyof TInput]: {
10
- base: k;
11
- /** The id that uniqueness is checked against. Either the primary key of the table, or the id you are joining **to** on a join table. */
12
- id: keyof NonNullable<TInput[k]>;
13
- joinsTo: keyof NonNullable<TInput[k]>;
14
- joinsFrom: keyof NonNullable<TJoinedFrom>;
15
- joinType: "single" | "array";
16
- } & RelationMapJoiner<TInput, TInput[k]>;
14
+ [n in keyof IsNullable]: {
15
+ base: k;
16
+ /** The id that uniqueness is checked against. Either the primary key of the table, or the id you are joining **to** on a join table. */
17
+ id: keyof NonNullable<TInput[k]>;
18
+ joinsTo: keyof NonNullable<TInput[k]>;
19
+ joinsFrom: keyof NonNullable<TJoinedFrom>;
20
+ joinType: "single" | "array";
21
+ isNullable?: IsNullable[n];
22
+ };
23
+ }[keyof IsNullable] & RelationMapJoiner<TInput, TInput[k]>;
17
24
  }[keyof TInput];
18
25
  export type RelationMapJoiner<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase> = {
19
26
  [k: `_${string}`]: RelationMapBase<TInput, TJoinedFrom> & RelationMapJoiner<TInput, TJoinedFrom>;
@@ -1,13 +1,16 @@
1
1
  import type { TInputBase, TJoinedFromBase, TOutputBase } from "./generic-bases";
2
2
  import type { RelationMapRoot } from "./inputs";
3
3
  import type { ExtendsNull, NoUnderscore, Simplify } from "./utils";
4
- export type RelationMapperReturnRoot<TInput extends TInputBase, TOutput extends RelationMapRoot<TInput>> = Simplify<ExtendsNull<TInput[TOutput["base"]]> extends true ? (TInput[TOutput["base"]] & RelationMapperReturnJoinRoot<TInput, TOutput, TOutput["base"]>) | null : TInput[TOutput["base"]] & RelationMapperReturnJoinRoot<TInput, TOutput, TOutput["base"]>>;
4
+ type JoinedRoot<TInput extends TInputBase, TOutput extends RelationMapRoot<TInput>> = TInput[TOutput["base"]] & RelationMapperReturnJoinRoot<TInput, TOutput, TOutput["base"]>;
5
+ export type RelationMapperReturnRoot<TInput extends TInputBase, TOutput extends RelationMapRoot<TInput>> = Simplify<ExtendsNull<TInput[TOutput["base"]]> extends true ? JoinedRoot<TInput, TOutput> | null : JoinedRoot<TInput, TOutput>>;
5
6
  export type RelationMapperReturnJoinRoot<TInput extends TInputBase, TOutput extends RelationMapRoot<TInput>, TInputKey extends keyof TInput> = {
6
7
  [outputKey in NoUnderscore<keyof TOutput>]: RelationMapperReturn<TInput, TInput[TInputKey], TOutput[`_${outputKey}`]>;
7
8
  };
9
+ export type Joined<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase, TOutput extends TOutputBase<TInput, TJoinedFrom>> = Simplify<TInput[TOutput["base"]] & RelationMapperReturnJoin<TInput, TJoinedFrom, TOutput>>;
8
10
  /** Fill current object with value specified in base */
9
- export type RelationMapperReturn<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase, TOutput extends TOutputBase<TInput, TJoinedFrom>> = TOutput["joinType"] extends "array" ? Array<Simplify<TInput[TOutput["base"]] & RelationMapperReturnJoin<TInput, TJoinedFrom, TOutput>>> : ExtendsNull<TInput[TOutput["base"]]> extends true ? Simplify<TInput[TOutput["base"]] & RelationMapperReturnJoin<TInput, TJoinedFrom, TOutput>> | null : Simplify<TInput[TOutput["base"]] & RelationMapperReturnJoin<TInput, TJoinedFrom, TOutput>>;
11
+ export type RelationMapperReturn<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase, TOutput extends TOutputBase<TInput, TJoinedFrom>> = TOutput["joinType"] extends "array" ? Array<Joined<TInput, TJoinedFrom, TOutput>> : ExtendsNull<TInput[TOutput["base"]]> extends true ? TOutput["isNullable"] extends false ? Joined<TInput, TJoinedFrom, TOutput> : Joined<TInput, TJoinedFrom, TOutput> | null : Joined<TInput, TJoinedFrom, TOutput>;
10
12
  /** Takes underscored values (joins) and replaces their values with the actual output. */
11
13
  export type RelationMapperReturnJoin<TInput extends TInputBase, TJoinedFrom extends TJoinedFromBase, TOutput extends TOutputBase<TInput, TJoinedFrom>> = {
12
14
  [k in NoUnderscore<keyof TOutput>]: RelationMapperReturn<TInput, TJoinedFrom, TOutput[`_${k}`]>;
13
15
  };
16
+ export {};
@@ -24,6 +24,7 @@ export type A = DeepSimplify<RelationMapperReturnRoot<{
24
24
  joinsTo: "team_id";
25
25
  joinsFrom: "id";
26
26
  joinType: "single";
27
+ isNullable: false;
27
28
  _team: {
28
29
  base: "team";
29
30
  id: "id";
@@ -1,15 +1,10 @@
1
1
  const invertInput = (inputs) => {
2
2
  const invertedInput = inputs.reduce((final, row) => {
3
3
  Object.entries(row).forEach(([header, value]) => {
4
- if (final[header]) {
5
- if (value)
6
- final[header].push(value);
7
- }
8
- else {
9
- final[header] = value
10
- ? [value]
11
- : [];
12
- }
4
+ final[header] ??= [];
5
+ // Instantiated above.
6
+ if (value)
7
+ final[header].push(value);
13
8
  });
14
9
  return final;
15
10
  }, {});
@@ -1 +1,6 @@
1
- export const getJoinFinalKey = (key) => key.substring(1);
1
+ export const getJoinFinalKey = (key) => {
2
+ if (key[0] !== "_") {
3
+ throw new Error("Joining key must start with underscore.");
4
+ }
5
+ return key.substring(1);
6
+ };