kanel-enum-tables 1.0.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/README.md +102 -0
- package/build/enumTablesPreRenderHook.d.ts +44 -0
- package/build/enumTablesPreRenderHook.js +299 -0
- package/build/enumTablesPreRenderHook.test.d.ts +1 -0
- package/build/enumTablesPreRenderHook.test.js +202 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +13 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Kanel extension to generate enum types from table values
|
|
2
|
+
|
|
3
|
+
This package extends [Kanel](https://github.com/kristiandupont/kanel) with the ability to generate enum types from table values.
|
|
4
|
+
|
|
5
|
+
It uses the [Postgraphile](https://www.graphile.org/postgraphile) concept of [enum tables](https://www.graphile.org/postgraphile/enums/#with-enum-tables).
|
|
6
|
+
|
|
7
|
+
## Smart Comments
|
|
8
|
+
|
|
9
|
+
Mark a table as an enum table by adding a `@enum` tag to its comment:
|
|
10
|
+
|
|
11
|
+
```sql
|
|
12
|
+
COMMENT ON TABLE animal_type IS '@enum';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You can optionally rename the generated type with `@enumName`:
|
|
16
|
+
|
|
17
|
+
```sql
|
|
18
|
+
COMMENT ON TABLE animal_type IS E'@enum\n@enumName TypeOfAnimal';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both Postgraphile's space format and the colon format used by other Kanel extensions are supported:
|
|
22
|
+
|
|
23
|
+
- `@enumName TypeOfAnimal` (Postgraphile convention)
|
|
24
|
+
- `@enumName:TypeOfAnimal` (tagged-comment-parser convention)
|
|
25
|
+
|
|
26
|
+
### `@enumDescription`
|
|
27
|
+
|
|
28
|
+
You can add per-value descriptions to your enum by tagging a column with `@enumDescription`. The values in that column will be rendered as JSDoc comments on each enum member.
|
|
29
|
+
|
|
30
|
+
```sql
|
|
31
|
+
CREATE TABLE animal_type (
|
|
32
|
+
type text PRIMARY KEY,
|
|
33
|
+
description text NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
COMMENT ON TABLE animal_type IS '@enum';
|
|
37
|
+
COMMENT ON COLUMN animal_type.description IS '@enumDescription';
|
|
38
|
+
|
|
39
|
+
INSERT INTO animal_type VALUES
|
|
40
|
+
('cat', 'A small domesticated feline'),
|
|
41
|
+
('dog', 'A loyal canine companion');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With `enumStyle: "enum"` this generates:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
export enum AnimalType {
|
|
48
|
+
/** A small domesticated feline */
|
|
49
|
+
cat = "cat",
|
|
50
|
+
/** A loyal canine companion */
|
|
51
|
+
dog = "dog",
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With `enumStyle: "type"` (the default) this generates:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
export type AnimalType =
|
|
59
|
+
/** A small domesticated feline */
|
|
60
|
+
| "cat"
|
|
61
|
+
/** A loyal canine companion */
|
|
62
|
+
| "dog";
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
Assuming you already have Kanel installed, add this with
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm i -D kanel-enum-tables
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
Add the hook to your `.kanelrc.js` file:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const { enumTablesPreRenderHook } = require("kanel-enum-tables");
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
// ... your config here.
|
|
82
|
+
|
|
83
|
+
preRenderHooks: [enumTablesPreRenderHook],
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### With kanel-kysely
|
|
88
|
+
|
|
89
|
+
When using [kanel-kysely](https://github.com/kristiandupont/kanel/tree/main/packages/kanel-kysely), place `enumTablesPreRenderHook` **before** the kysely hook for best results:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
const { enumTablesPreRenderHook } = require("kanel-enum-tables");
|
|
93
|
+
const { makeKyselyHook } = require("kanel-kysely");
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
// ... your config here.
|
|
97
|
+
|
|
98
|
+
preRenderHooks: [enumTablesPreRenderHook, makeKyselyHook()],
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The hook also works if placed after the kysely hook — it will detect the `ColumnType<>` wrappers and update them accordingly.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TableDetails } from "extract-pg-schema";
|
|
2
|
+
import type { PreRenderHook } from "kanel";
|
|
3
|
+
/**
|
|
4
|
+
* Parse smart comment tags from a database comment string.
|
|
5
|
+
*
|
|
6
|
+
* Supports both Postgraphile's newline-separated format and
|
|
7
|
+
* tagged-comment-parser's colon format:
|
|
8
|
+
*
|
|
9
|
+
* Postgraphile: `E'@enum\n@enumName TypeOfAnimal'`
|
|
10
|
+
* Colon: `@enum @enumName:TypeOfAnimal`
|
|
11
|
+
*
|
|
12
|
+
* Since tagged-comment-parser doesn't support newline-separated tags,
|
|
13
|
+
* we parse each line individually and merge the results.
|
|
14
|
+
*/
|
|
15
|
+
export declare const parseSmartTags: (comment: string | null) => Record<string, string | boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the value of a smart tag that may have been parsed as a boolean
|
|
18
|
+
* (Postgraphile space format) instead of a string (colon format).
|
|
19
|
+
*
|
|
20
|
+
* When tagged-comment-parser encounters `@enumName TypeOfAnimal`,
|
|
21
|
+
* it parses `enumName` as `true` and puts "TypeOfAnimal" in the comment.
|
|
22
|
+
* We fall back to regex extraction from the raw comment in that case.
|
|
23
|
+
*/
|
|
24
|
+
export declare const resolveTagValue: (tags: Record<string, string | boolean>, tagName: string, rawComment: string) => string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file has already been processed by kanel-kysely.
|
|
27
|
+
*
|
|
28
|
+
* kanel-kysely replaces selector interfaces with *Table interfaces
|
|
29
|
+
* that use ColumnType<> wrappers, and removes initializer/mutator interfaces.
|
|
30
|
+
* We detect this by looking for ColumnType in type imports.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isKyselyProcessed: (declarations: unknown[]) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Update a ColumnType<S, I, M> string by replacing all occurrences of
|
|
35
|
+
* the old type name with the new enum type name.
|
|
36
|
+
*/
|
|
37
|
+
export declare const updateColumnType: (typeName: string, oldName: string, newName: string) => string;
|
|
38
|
+
/**
|
|
39
|
+
* Find the column name marked with @enumDescription in the table's column comments.
|
|
40
|
+
* Returns the column name if found, undefined otherwise.
|
|
41
|
+
*/
|
|
42
|
+
export declare const findDescriptionColumn: (table: TableDetails) => string | undefined;
|
|
43
|
+
declare const enumTablesPreRenderHook: PreRenderHook;
|
|
44
|
+
export default enumTablesPreRenderHook;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findDescriptionColumn = exports.updateColumnType = exports.isKyselyProcessed = exports.resolveTagValue = exports.parseSmartTags = void 0;
|
|
7
|
+
const kanel_1 = require("kanel");
|
|
8
|
+
const knex_1 = __importDefault(require("knex"));
|
|
9
|
+
const tagged_comment_parser_1 = require("tagged-comment-parser");
|
|
10
|
+
/**
|
|
11
|
+
* Parse smart comment tags from a database comment string.
|
|
12
|
+
*
|
|
13
|
+
* Supports both Postgraphile's newline-separated format and
|
|
14
|
+
* tagged-comment-parser's colon format:
|
|
15
|
+
*
|
|
16
|
+
* Postgraphile: `E'@enum\n@enumName TypeOfAnimal'`
|
|
17
|
+
* Colon: `@enum @enumName:TypeOfAnimal`
|
|
18
|
+
*
|
|
19
|
+
* Since tagged-comment-parser doesn't support newline-separated tags,
|
|
20
|
+
* we parse each line individually and merge the results.
|
|
21
|
+
*/
|
|
22
|
+
const parseSmartTags = (comment) => {
|
|
23
|
+
if (!comment)
|
|
24
|
+
return {};
|
|
25
|
+
const mergedTags = {};
|
|
26
|
+
for (const line of comment.split("\n")) {
|
|
27
|
+
const { tags } = (0, tagged_comment_parser_1.tryParse)(line.trim());
|
|
28
|
+
if (tags) {
|
|
29
|
+
Object.assign(mergedTags, tags);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return mergedTags;
|
|
33
|
+
};
|
|
34
|
+
exports.parseSmartTags = parseSmartTags;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the value of a smart tag that may have been parsed as a boolean
|
|
37
|
+
* (Postgraphile space format) instead of a string (colon format).
|
|
38
|
+
*
|
|
39
|
+
* When tagged-comment-parser encounters `@enumName TypeOfAnimal`,
|
|
40
|
+
* it parses `enumName` as `true` and puts "TypeOfAnimal" in the comment.
|
|
41
|
+
* We fall back to regex extraction from the raw comment in that case.
|
|
42
|
+
*/
|
|
43
|
+
const resolveTagValue = (tags, tagName, rawComment) => {
|
|
44
|
+
const value = tags[tagName];
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (value === true) {
|
|
49
|
+
// Postgraphile space format — extract value from raw comment
|
|
50
|
+
const match = rawComment.match(new RegExp(`@${tagName}\\s+(\\S+)`));
|
|
51
|
+
return match?.[1];
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
exports.resolveTagValue = resolveTagValue;
|
|
56
|
+
/**
|
|
57
|
+
* Check if a file has already been processed by kanel-kysely.
|
|
58
|
+
*
|
|
59
|
+
* kanel-kysely replaces selector interfaces with *Table interfaces
|
|
60
|
+
* that use ColumnType<> wrappers, and removes initializer/mutator interfaces.
|
|
61
|
+
* We detect this by looking for ColumnType in type imports.
|
|
62
|
+
*/
|
|
63
|
+
const isKyselyProcessed = (declarations) => declarations.some((d) => d?.declarationType === "interface" &&
|
|
64
|
+
d?.typeImports?.some((i) => i.name === "ColumnType" && i.path === "kysely"));
|
|
65
|
+
exports.isKyselyProcessed = isKyselyProcessed;
|
|
66
|
+
/**
|
|
67
|
+
* Update a ColumnType<S, I, M> string by replacing all occurrences of
|
|
68
|
+
* the old type name with the new enum type name.
|
|
69
|
+
*/
|
|
70
|
+
const updateColumnType = (typeName, oldName, newName) => {
|
|
71
|
+
if (!typeName.startsWith("ColumnType<"))
|
|
72
|
+
return typeName;
|
|
73
|
+
return typeName.replaceAll(oldName, newName);
|
|
74
|
+
};
|
|
75
|
+
exports.updateColumnType = updateColumnType;
|
|
76
|
+
/**
|
|
77
|
+
* Find the column name marked with @enumDescription in the table's column comments.
|
|
78
|
+
* Returns the column name if found, undefined otherwise.
|
|
79
|
+
*/
|
|
80
|
+
const findDescriptionColumn = (table) => {
|
|
81
|
+
for (const column of table.columns) {
|
|
82
|
+
const tags = (0, exports.parseSmartTags)(column.comment ?? null);
|
|
83
|
+
if (tags.enumDescription) {
|
|
84
|
+
return column.name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
exports.findDescriptionColumn = findDescriptionColumn;
|
|
90
|
+
const enumTablesPreRenderHook = async (outputAccumulator, instantiatedConfig) => {
|
|
91
|
+
if (!instantiatedConfig.generateIdentifierType) {
|
|
92
|
+
console.warn("kanel-enum-tables: generateIdentifierType is not configured, skipping enum table generation");
|
|
93
|
+
return outputAccumulator;
|
|
94
|
+
}
|
|
95
|
+
// Find all tables tagged with @enum
|
|
96
|
+
const enumTables = [];
|
|
97
|
+
for (const schema of Object.values(instantiatedConfig.schemas)) {
|
|
98
|
+
for (const table of schema.tables) {
|
|
99
|
+
const tags = (0, exports.parseSmartTags)(table.comment);
|
|
100
|
+
if (tags.enum) {
|
|
101
|
+
enumTables.push({
|
|
102
|
+
table,
|
|
103
|
+
enumName: (0, exports.resolveTagValue)(tags, "enumName", table.comment),
|
|
104
|
+
descriptionColumn: (0, exports.findDescriptionColumn)(table),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (enumTables.length === 0) {
|
|
110
|
+
return outputAccumulator;
|
|
111
|
+
}
|
|
112
|
+
const connection = instantiatedConfig.connection;
|
|
113
|
+
const db = (0, knex_1.default)({ client: "postgres", connection });
|
|
114
|
+
try {
|
|
115
|
+
const overrides = {};
|
|
116
|
+
for (const { table, enumName, descriptionColumn } of enumTables) {
|
|
117
|
+
const primaryKeyColumn = table.columns.find((column) => column.isPrimaryKey);
|
|
118
|
+
if (!primaryKeyColumn) {
|
|
119
|
+
throw new Error(`Table ${table.schemaName}.${table.name} is an enum table but has no primary key`);
|
|
120
|
+
}
|
|
121
|
+
const primaryKeyTypeDeclaration = instantiatedConfig.generateIdentifierType(primaryKeyColumn, table, instantiatedConfig);
|
|
122
|
+
// Get the resolved type for the primary key column.
|
|
123
|
+
// Pass `true` for retainInnerIdentifierType to skip identifier type
|
|
124
|
+
// wrapping and get the actual underlying type (e.g. "string").
|
|
125
|
+
const primaryKeyInnerType = (0, kanel_1.resolveType)(primaryKeyColumn, table, true);
|
|
126
|
+
if (primaryKeyInnerType !== "string") {
|
|
127
|
+
throw new Error(`The primary key of enum table ${table.schemaName}.${table.name} must be string-based, got: ${primaryKeyInnerType}`);
|
|
128
|
+
}
|
|
129
|
+
const selectorMetadata = instantiatedConfig.getMetadata(table, "selector", instantiatedConfig);
|
|
130
|
+
const initializerMetadata = instantiatedConfig.getMetadata(table, "initializer", instantiatedConfig);
|
|
131
|
+
const mutatorMetadata = instantiatedConfig.getMetadata(table, "mutator", instantiatedConfig);
|
|
132
|
+
// @enumDescription marks a column whose values provide per-enum-value descriptions.
|
|
133
|
+
// When enumStyle is "type", descriptions are rendered as inline JSDoc comments.
|
|
134
|
+
// When enumStyle is "enum", we use a GenericDeclaration to emit hand-crafted
|
|
135
|
+
// enum lines with per-value JSDoc comments (since EnumDeclaration.values is string-only).
|
|
136
|
+
const selectColumns = [primaryKeyColumn.name];
|
|
137
|
+
if (descriptionColumn) {
|
|
138
|
+
selectColumns.push(descriptionColumn);
|
|
139
|
+
}
|
|
140
|
+
const rows = await db
|
|
141
|
+
.withSchema(table.schemaName)
|
|
142
|
+
.select(...selectColumns)
|
|
143
|
+
.from(table.name)
|
|
144
|
+
.orderBy(primaryKeyColumn.name);
|
|
145
|
+
const existingFile = outputAccumulator[selectorMetadata.path];
|
|
146
|
+
const declarations = existingFile?.declarations ?? [];
|
|
147
|
+
const kyselyMode = (0, exports.isKyselyProcessed)(declarations);
|
|
148
|
+
const finalEnumName = enumName || primaryKeyTypeDeclaration.name;
|
|
149
|
+
const newDeclarations = declarations.map((declaration) => {
|
|
150
|
+
// Replace the identifier TypeDeclaration with enum values
|
|
151
|
+
if (declaration.declarationType === "typeDeclaration" &&
|
|
152
|
+
declaration.name === primaryKeyTypeDeclaration.name) {
|
|
153
|
+
if (instantiatedConfig.enumStyle === "type") {
|
|
154
|
+
const typeLines = [""];
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
const desc = descriptionColumn && row[descriptionColumn]
|
|
157
|
+
? String(row[descriptionColumn])
|
|
158
|
+
: undefined;
|
|
159
|
+
if (desc) {
|
|
160
|
+
typeLines.push(`/** ${desc} */`);
|
|
161
|
+
}
|
|
162
|
+
typeLines.push(`| '${row[primaryKeyColumn.name]}'`);
|
|
163
|
+
}
|
|
164
|
+
const newDeclaration = {
|
|
165
|
+
...declaration,
|
|
166
|
+
name: finalEnumName,
|
|
167
|
+
typeDefinition: typeLines,
|
|
168
|
+
};
|
|
169
|
+
return newDeclaration;
|
|
170
|
+
}
|
|
171
|
+
else if (instantiatedConfig.enumStyle === "enum") {
|
|
172
|
+
if (descriptionColumn) {
|
|
173
|
+
// Use GenericDeclaration to emit per-value JSDoc comments
|
|
174
|
+
const lines = [];
|
|
175
|
+
if (declaration.exportAs === "named") {
|
|
176
|
+
lines.push(`export enum ${finalEnumName} {`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
lines.push(`enum ${finalEnumName} {`);
|
|
180
|
+
}
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
const value = String(row[primaryKeyColumn.name]);
|
|
183
|
+
const desc = row[descriptionColumn]
|
|
184
|
+
? String(row[descriptionColumn])
|
|
185
|
+
: undefined;
|
|
186
|
+
if (desc) {
|
|
187
|
+
lines.push(` /** ${desc} */`);
|
|
188
|
+
}
|
|
189
|
+
// Quote the field name if it contains special characters
|
|
190
|
+
const needsQuote = value.length === 0 ||
|
|
191
|
+
value.trim() !== value ||
|
|
192
|
+
!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
|
|
193
|
+
const fieldName = needsQuote ? `'${value}'` : value;
|
|
194
|
+
lines.push(` ${fieldName} = '${value}',`);
|
|
195
|
+
}
|
|
196
|
+
lines.push("};");
|
|
197
|
+
if (declaration.exportAs === "default") {
|
|
198
|
+
lines.push("", `export default ${finalEnumName};`);
|
|
199
|
+
}
|
|
200
|
+
const newDeclaration = {
|
|
201
|
+
declarationType: "generic",
|
|
202
|
+
comment: declaration.comment,
|
|
203
|
+
typeImports: declaration.typeImports,
|
|
204
|
+
lines,
|
|
205
|
+
};
|
|
206
|
+
return newDeclaration;
|
|
207
|
+
}
|
|
208
|
+
const newDeclaration = {
|
|
209
|
+
declarationType: "enum",
|
|
210
|
+
name: finalEnumName,
|
|
211
|
+
comment: declaration.comment,
|
|
212
|
+
exportAs: declaration.exportAs,
|
|
213
|
+
values: rows.map((row) => row[primaryKeyColumn.name]),
|
|
214
|
+
typeImports: declaration.typeImports,
|
|
215
|
+
};
|
|
216
|
+
return newDeclaration;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.warn(`Unsupported enumStyle "${instantiatedConfig.enumStyle}" for enum table ${table.schemaName}.${table.name}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Handle initializer/mutator interfaces (not present when kanel-kysely runs first)
|
|
223
|
+
if (!kyselyMode &&
|
|
224
|
+
declaration.declarationType === "interface" &&
|
|
225
|
+
(declaration.name === initializerMetadata.name ||
|
|
226
|
+
declaration.name === mutatorMetadata.name)) {
|
|
227
|
+
const newDeclaration = {
|
|
228
|
+
...declaration,
|
|
229
|
+
properties: declaration.properties.map((property) => {
|
|
230
|
+
if (property.name === primaryKeyColumn.name) {
|
|
231
|
+
return {
|
|
232
|
+
...property,
|
|
233
|
+
// When inserting/updating, accept any string as a new enum value
|
|
234
|
+
typeName: primaryKeyInnerType,
|
|
235
|
+
typeImports: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return property;
|
|
239
|
+
}),
|
|
240
|
+
};
|
|
241
|
+
return newDeclaration;
|
|
242
|
+
}
|
|
243
|
+
// Handle selector interface — update PK property when @enumName renames the type
|
|
244
|
+
if (!kyselyMode &&
|
|
245
|
+
declaration.declarationType === "interface" &&
|
|
246
|
+
declaration.name === selectorMetadata.name &&
|
|
247
|
+
enumName) {
|
|
248
|
+
const newDeclaration = {
|
|
249
|
+
...declaration,
|
|
250
|
+
properties: declaration.properties.map((property) => {
|
|
251
|
+
if (property.name === primaryKeyColumn.name) {
|
|
252
|
+
return {
|
|
253
|
+
...property,
|
|
254
|
+
typeName: enumName,
|
|
255
|
+
typeImports: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return property;
|
|
259
|
+
}),
|
|
260
|
+
};
|
|
261
|
+
return newDeclaration;
|
|
262
|
+
}
|
|
263
|
+
// Handle kanel-kysely *Table interface — update ColumnType<> references
|
|
264
|
+
if (kyselyMode &&
|
|
265
|
+
declaration.declarationType === "interface" &&
|
|
266
|
+
enumName &&
|
|
267
|
+
declaration.typeImports?.some((i) => i.name === "ColumnType" && i.path === "kysely")) {
|
|
268
|
+
const oldTypeName = primaryKeyTypeDeclaration.name;
|
|
269
|
+
const newDeclaration = {
|
|
270
|
+
...declaration,
|
|
271
|
+
properties: declaration.properties.map((property) => {
|
|
272
|
+
if (property.name === primaryKeyColumn.name) {
|
|
273
|
+
return {
|
|
274
|
+
...property,
|
|
275
|
+
typeName: (0, exports.updateColumnType)(property.typeName, oldTypeName, enumName),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return property;
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
return newDeclaration;
|
|
282
|
+
}
|
|
283
|
+
return declaration;
|
|
284
|
+
});
|
|
285
|
+
overrides[selectorMetadata.path] = {
|
|
286
|
+
...existingFile,
|
|
287
|
+
declarations: newDeclarations,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
...outputAccumulator,
|
|
292
|
+
...overrides,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await db.destroy();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
exports.default = enumTablesPreRenderHook;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const enumTablesPreRenderHook_1 = require("./enumTablesPreRenderHook");
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// parseSmartTags
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
(0, vitest_1.describe)("parseSmartTags", () => {
|
|
9
|
+
(0, vitest_1.it)("should return empty object for null comment", () => {
|
|
10
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.parseSmartTags)(null)).toEqual({});
|
|
11
|
+
});
|
|
12
|
+
(0, vitest_1.it)("should return empty object for empty string", () => {
|
|
13
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.parseSmartTags)("")).toEqual({});
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)("should return empty object for comment without tags", () => {
|
|
16
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.parseSmartTags)("just a plain comment")).toEqual({});
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)("should parse a single @enum tag", () => {
|
|
19
|
+
const tags = (0, enumTablesPreRenderHook_1.parseSmartTags)("@enum");
|
|
20
|
+
(0, vitest_1.expect)(tags).toHaveProperty("enum");
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)("should parse colon-format tag value", () => {
|
|
23
|
+
const tags = (0, enumTablesPreRenderHook_1.parseSmartTags)("@enumName:TypeOfAnimal");
|
|
24
|
+
(0, vitest_1.expect)(tags.enumName).toBe("TypeOfAnimal");
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)("should parse newline-separated tags (Postgraphile format)", () => {
|
|
27
|
+
const tags = (0, enumTablesPreRenderHook_1.parseSmartTags)("@enum\n@enumName TypeOfAnimal");
|
|
28
|
+
(0, vitest_1.expect)(tags).toHaveProperty("enum");
|
|
29
|
+
(0, vitest_1.expect)(tags).toHaveProperty("enumName");
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)("should handle multiple tags on one line", () => {
|
|
32
|
+
const tags = (0, enumTablesPreRenderHook_1.parseSmartTags)("@enum @enumName:Foo");
|
|
33
|
+
(0, vitest_1.expect)(tags).toHaveProperty("enum");
|
|
34
|
+
(0, vitest_1.expect)(tags.enumName).toBe("Foo");
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)("should handle extra whitespace around lines", () => {
|
|
37
|
+
const tags = (0, enumTablesPreRenderHook_1.parseSmartTags)(" @enum \n @enumName:Bar ");
|
|
38
|
+
(0, vitest_1.expect)(tags).toHaveProperty("enum");
|
|
39
|
+
(0, vitest_1.expect)(tags.enumName).toBe("Bar");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// resolveTagValue
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
(0, vitest_1.describe)("resolveTagValue", () => {
|
|
46
|
+
(0, vitest_1.it)("should return string value directly (colon format)", () => {
|
|
47
|
+
const tags = { enumName: "TypeOfAnimal" };
|
|
48
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.resolveTagValue)(tags, "enumName", "@enumName:TypeOfAnimal")).toBe("TypeOfAnimal");
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)("should extract value from raw comment when tag is boolean (space format)", () => {
|
|
51
|
+
// tagged-comment-parser parses `@enumName TypeOfAnimal` as enumName: true
|
|
52
|
+
const tags = { enumName: true };
|
|
53
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.resolveTagValue)(tags, "enumName", "@enumName TypeOfAnimal")).toBe("TypeOfAnimal");
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)("should return undefined when tag is not present", () => {
|
|
56
|
+
const tags = { enum: true };
|
|
57
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.resolveTagValue)(tags, "enumName", "@enum")).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)("should handle multiline raw comment for space format extraction", () => {
|
|
60
|
+
const raw = "@enum\n@enumName TypeOfAnimal";
|
|
61
|
+
const tags = { enum: true, enumName: true };
|
|
62
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.resolveTagValue)(tags, "enumName", raw)).toBe("TypeOfAnimal");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// isKyselyProcessed
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
(0, vitest_1.describe)("isKyselyProcessed", () => {
|
|
69
|
+
(0, vitest_1.it)("should return true when ColumnType import from kysely is present", () => {
|
|
70
|
+
const declarations = [
|
|
71
|
+
{
|
|
72
|
+
declarationType: "interface",
|
|
73
|
+
name: "MemberTable",
|
|
74
|
+
typeImports: [{ name: "ColumnType", path: "kysely" }],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.isKyselyProcessed)(declarations)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)("should return false without ColumnType import", () => {
|
|
80
|
+
const declarations = [
|
|
81
|
+
{
|
|
82
|
+
declarationType: "interface",
|
|
83
|
+
name: "Member",
|
|
84
|
+
typeImports: [{ name: "SomeType", path: "./types" }],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.isKyselyProcessed)(declarations)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)("should return false for empty declarations", () => {
|
|
90
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.isKyselyProcessed)([])).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("should return false when ColumnType is imported from a different path", () => {
|
|
93
|
+
const declarations = [
|
|
94
|
+
{
|
|
95
|
+
declarationType: "interface",
|
|
96
|
+
name: "Member",
|
|
97
|
+
typeImports: [{ name: "ColumnType", path: "other-lib" }],
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.isKyselyProcessed)(declarations)).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
(0, vitest_1.it)("should return false for non-interface declarations", () => {
|
|
103
|
+
const declarations = [
|
|
104
|
+
{
|
|
105
|
+
declarationType: "typeDeclaration",
|
|
106
|
+
name: "MemberId",
|
|
107
|
+
typeImports: [{ name: "ColumnType", path: "kysely" }],
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.isKyselyProcessed)(declarations)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// updateColumnType
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
(0, vitest_1.describe)("updateColumnType", () => {
|
|
117
|
+
(0, vitest_1.it)("should replace type name inside ColumnType<>", () => {
|
|
118
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.updateColumnType)("ColumnType<MemberId, MemberId, MemberId>", "MemberId", "MemberType")).toBe("ColumnType<MemberType, MemberType, MemberType>");
|
|
119
|
+
});
|
|
120
|
+
(0, vitest_1.it)("should handle ColumnType with different inner types", () => {
|
|
121
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.updateColumnType)("ColumnType<MemberId, string | MemberId, MemberId>", "MemberId", "MemberType")).toBe("ColumnType<MemberType, string | MemberType, MemberType>");
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.it)("should return the type unchanged when not a ColumnType", () => {
|
|
124
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.updateColumnType)("MemberId", "MemberId", "MemberType")).toBe("MemberId");
|
|
125
|
+
});
|
|
126
|
+
(0, vitest_1.it)("should return empty ColumnType unchanged when no match", () => {
|
|
127
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.updateColumnType)("ColumnType<Foo, Bar, Baz>", "Other", "New")).toBe("ColumnType<Foo, Bar, Baz>");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// findDescriptionColumn
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
const makeColumn = (name, comment = null) => ({
|
|
134
|
+
name,
|
|
135
|
+
comment,
|
|
136
|
+
expandedType: "pg_catalog.text",
|
|
137
|
+
type: { fullName: "pg_catalog.text", kind: "base" },
|
|
138
|
+
defaultValue: null,
|
|
139
|
+
isArray: false,
|
|
140
|
+
dimensions: 0,
|
|
141
|
+
references: [],
|
|
142
|
+
reference: null,
|
|
143
|
+
indices: [],
|
|
144
|
+
maxLength: null,
|
|
145
|
+
isNullable: false,
|
|
146
|
+
isPrimaryKey: name === "id",
|
|
147
|
+
generated: "NEVER",
|
|
148
|
+
isUpdatable: true,
|
|
149
|
+
isIdentity: false,
|
|
150
|
+
ordinalPosition: 1,
|
|
151
|
+
parentTable: null,
|
|
152
|
+
informationSchemaValue: {},
|
|
153
|
+
});
|
|
154
|
+
const makeTable = (name, columns, comment = null) => ({
|
|
155
|
+
name,
|
|
156
|
+
schemaName: "public",
|
|
157
|
+
kind: "table",
|
|
158
|
+
comment,
|
|
159
|
+
columns,
|
|
160
|
+
indices: [],
|
|
161
|
+
checks: [],
|
|
162
|
+
isRowLevelSecurityEnabled: false,
|
|
163
|
+
isRowLevelSecurityEnforced: false,
|
|
164
|
+
securityPolicies: [],
|
|
165
|
+
triggers: [],
|
|
166
|
+
informationSchemaValue: {},
|
|
167
|
+
});
|
|
168
|
+
(0, vitest_1.describe)("findDescriptionColumn", () => {
|
|
169
|
+
(0, vitest_1.it)("should return column name when @enumDescription tag is present", () => {
|
|
170
|
+
const table = makeTable("member_type", [
|
|
171
|
+
makeColumn("id"),
|
|
172
|
+
makeColumn("description", "@enumDescription"),
|
|
173
|
+
]);
|
|
174
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.findDescriptionColumn)(table)).toBe("description");
|
|
175
|
+
});
|
|
176
|
+
(0, vitest_1.it)("should return undefined when no column has @enumDescription", () => {
|
|
177
|
+
const table = makeTable("member_type", [
|
|
178
|
+
makeColumn("id"),
|
|
179
|
+
makeColumn("label", "A human-readable label"),
|
|
180
|
+
]);
|
|
181
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.findDescriptionColumn)(table)).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
(0, vitest_1.it)("should handle @enumDescription with colon format", () => {
|
|
184
|
+
const table = makeTable("status", [
|
|
185
|
+
makeColumn("code"),
|
|
186
|
+
makeColumn("display_name", "@enumDescription:true"),
|
|
187
|
+
]);
|
|
188
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.findDescriptionColumn)(table)).toBe("display_name");
|
|
189
|
+
});
|
|
190
|
+
(0, vitest_1.it)("should return the first column with @enumDescription when multiple match", () => {
|
|
191
|
+
const table = makeTable("status", [
|
|
192
|
+
makeColumn("code"),
|
|
193
|
+
makeColumn("short_desc", "@enumDescription"),
|
|
194
|
+
makeColumn("long_desc", "@enumDescription"),
|
|
195
|
+
]);
|
|
196
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.findDescriptionColumn)(table)).toBe("short_desc");
|
|
197
|
+
});
|
|
198
|
+
(0, vitest_1.it)("should return undefined when table has no columns", () => {
|
|
199
|
+
const table = makeTable("empty_table", []);
|
|
200
|
+
(0, vitest_1.expect)((0, enumTablesPreRenderHook_1.findDescriptionColumn)(table)).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as enumTablesPreRenderHook, findDescriptionColumn, isKyselyProcessed, parseSmartTags, resolveTagValue, updateColumnType, } from "./enumTablesPreRenderHook";
|
package/build/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.updateColumnType = exports.resolveTagValue = exports.parseSmartTags = exports.isKyselyProcessed = exports.findDescriptionColumn = exports.enumTablesPreRenderHook = void 0;
|
|
7
|
+
var enumTablesPreRenderHook_1 = require("./enumTablesPreRenderHook");
|
|
8
|
+
Object.defineProperty(exports, "enumTablesPreRenderHook", { enumerable: true, get: function () { return __importDefault(enumTablesPreRenderHook_1).default; } });
|
|
9
|
+
Object.defineProperty(exports, "findDescriptionColumn", { enumerable: true, get: function () { return enumTablesPreRenderHook_1.findDescriptionColumn; } });
|
|
10
|
+
Object.defineProperty(exports, "isKyselyProcessed", { enumerable: true, get: function () { return enumTablesPreRenderHook_1.isKyselyProcessed; } });
|
|
11
|
+
Object.defineProperty(exports, "parseSmartTags", { enumerable: true, get: function () { return enumTablesPreRenderHook_1.parseSmartTags; } });
|
|
12
|
+
Object.defineProperty(exports, "resolveTagValue", { enumerable: true, get: function () { return enumTablesPreRenderHook_1.resolveTagValue; } });
|
|
13
|
+
Object.defineProperty(exports, "updateColumnType", { enumerable: true, get: function () { return enumTablesPreRenderHook_1.updateColumnType; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kanel-enum-tables",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"enum",
|
|
6
|
+
"postgresql",
|
|
7
|
+
"schema",
|
|
8
|
+
"typescript"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kristiandupont/kanel.git"
|
|
13
|
+
},
|
|
14
|
+
"description": "Kanel extension for generating enums from table values",
|
|
15
|
+
"homepage": "https://kristiandupont.github.io/kanel",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Simon Fridlund",
|
|
18
|
+
"url": "http://github.com/zimme"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"build/"
|
|
22
|
+
],
|
|
23
|
+
"main": "build/index.js",
|
|
24
|
+
"types": "build/index.d.ts",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf ./build && tsc",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"knex": "^3.0.0",
|
|
32
|
+
"tagged-comment-parser": "^1.3.8"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"extract-pg-schema": "^5.8.1",
|
|
36
|
+
"kanel": "*",
|
|
37
|
+
"vitest": "^4.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|