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/.vscode/settings.json +4 -4
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-4.13.0.cjs +940 -940
- package/.yarnrc.yml +3 -3
- package/README.md +181 -0
- package/dist/src/index.js +6 -1
- package/dist/src/types/generic-less.d.ts +1 -0
- package/dist/src/types/inputs.d.ts +14 -7
- package/dist/src/types/return.d.ts +5 -2
- package/dist/src/types/typetest.d.ts +1 -0
- package/dist/src/utils/invertInput.js +4 -9
- package/dist/src/utils/keys.js +6 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -36
- package/src/index.test.ts +105 -105
- package/src/index.ts +160 -151
- package/src/testing/file-output.ts +15 -15
- package/src/testing/test-data.ts +172 -172
- package/src/types/generic-bases.ts +9 -9
- package/src/types/generic-less.ts +15 -14
- package/src/types/index.ts +5 -5
- package/src/types/inputs.ts +36 -30
- package/src/types/return.ts +65 -67
- package/src/types/typetest.ts +87 -84
- package/src/types/utils.ts +28 -28
- package/src/utils/invertInput.ts +25 -28
- package/src/utils/keys.ts +7 -1
- package/src/utils/matcher.ts +8 -8
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
|
|
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,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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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<
|
|
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 {};
|
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
}, {});
|