@izumisy-tailor/tailor-data-viewer 0.1.10 → 0.1.12
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 +5 -17
- package/dist/generator/index.d.mts +38 -2
- package/dist/generator/index.mjs +94 -86
- package/package.json +1 -1
- package/src/component/hooks/use-table-data.ts +13 -0
- package/src/component/index.ts +0 -10
- package/src/component/single-record-tab-content.tsx +1 -2
- package/src/generator/metadata-generator.ts +184 -161
package/README.md
CHANGED
|
@@ -98,20 +98,6 @@ function App() {
|
|
|
98
98
|
|
|
99
99
|
For store options, see [Saved View Store](./docs/saved-view-store.md).
|
|
100
100
|
|
|
101
|
-
### Using Individual Components
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
import {
|
|
105
|
-
DataTable,
|
|
106
|
-
TableSelector,
|
|
107
|
-
ColumnSelector,
|
|
108
|
-
Pagination,
|
|
109
|
-
SearchFilterForm,
|
|
110
|
-
useTableData,
|
|
111
|
-
useColumnState,
|
|
112
|
-
} from "@izumisy-tailor/tailor-data-viewer/component";
|
|
113
|
-
```
|
|
114
|
-
|
|
115
101
|
### Generating Table Metadata
|
|
116
102
|
|
|
117
103
|
This library includes a custom generator for [Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/sdk) that automatically generates table metadata from your TailorDB schema.
|
|
@@ -128,10 +114,12 @@ npm install @izumisy-tailor/tailor-data-viewer
|
|
|
128
114
|
|
|
129
115
|
```typescript
|
|
130
116
|
import { defineConfig, defineGenerators } from "@tailor-platform/sdk";
|
|
131
|
-
import
|
|
117
|
+
import { dataViewerMetadataGenerator } from "@izumisy-tailor/tailor-data-viewer/generator";
|
|
132
118
|
|
|
133
119
|
export const generators = defineGenerators(
|
|
134
|
-
|
|
120
|
+
dataViewerMetadataGenerator({
|
|
121
|
+
distPath: "src/generated/data-viewer-metadata.generated.ts",
|
|
122
|
+
}),
|
|
135
123
|
// ... other generators
|
|
136
124
|
);
|
|
137
125
|
|
|
@@ -147,7 +135,7 @@ export default defineConfig({
|
|
|
147
135
|
tailor-sdk generate
|
|
148
136
|
```
|
|
149
137
|
|
|
150
|
-
This will generate a
|
|
138
|
+
This will generate a metadata file at the specified `distPath` containing type-safe metadata for all your TailorDB types, including fields, relations, and role-based access control settings.
|
|
151
139
|
|
|
152
140
|
## API Reference
|
|
153
141
|
|
|
@@ -148,7 +148,43 @@ interface GeneratorResult {
|
|
|
148
148
|
errors?: string[];
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
151
|
+
* Options for the data viewer metadata generator
|
|
152
|
+
*/
|
|
153
|
+
interface DataViewerMetadataGeneratorOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Output file path relative to project root
|
|
156
|
+
* @default "data-viewer-metadata.generated.ts"
|
|
157
|
+
*/
|
|
158
|
+
distPath?: string;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Creates a custom generator that extracts table metadata for Data View
|
|
162
|
+
*/
|
|
163
|
+
declare function dataViewerMetadataGenerator(options?: DataViewerMetadataGeneratorOptions): {
|
|
164
|
+
id: string;
|
|
165
|
+
description: string;
|
|
166
|
+
dependencies: ("tailordb" | "resolver" | "executor")[];
|
|
167
|
+
processType({
|
|
168
|
+
type
|
|
169
|
+
}: {
|
|
170
|
+
type: ParsedTailorDBType;
|
|
171
|
+
}): ProcessedTable;
|
|
172
|
+
processResolver(): null;
|
|
173
|
+
processExecutor(): null;
|
|
174
|
+
processTailorDBNamespace({
|
|
175
|
+
types
|
|
176
|
+
}: {
|
|
177
|
+
types: Record<string, ProcessedTable>;
|
|
178
|
+
}): ProcessedTable[];
|
|
179
|
+
aggregate({
|
|
180
|
+
input
|
|
181
|
+
}: {
|
|
182
|
+
input: GeneratorInput;
|
|
183
|
+
}): GeneratorResult;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Default table metadata generator instance
|
|
187
|
+
* @deprecated Use `dataViewerMetadataGenerator()` instead for custom output path
|
|
152
188
|
*/
|
|
153
189
|
declare const tableMetadataGenerator: {
|
|
154
190
|
id: string;
|
|
@@ -173,4 +209,4 @@ declare const tableMetadataGenerator: {
|
|
|
173
209
|
}): GeneratorResult;
|
|
174
210
|
};
|
|
175
211
|
//#endregion
|
|
176
|
-
export { ExpandedRelationFields, FieldMetadata, FieldType, RelationMetadata, TableMetadata, TableMetadataMap, tableMetadataGenerator as default, tableMetadataGenerator };
|
|
212
|
+
export { DataViewerMetadataGeneratorOptions, ExpandedRelationFields, FieldMetadata, FieldType, RelationMetadata, TableMetadata, TableMetadataMap, dataViewerMetadataGenerator, tableMetadataGenerator as default, tableMetadataGenerator };
|
package/dist/generator/index.mjs
CHANGED
|
@@ -48,92 +48,94 @@ function extractReadAllowedRoles(gqlPermission) {
|
|
|
48
48
|
return Array.from(roles);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* Creates a custom generator that extracts table metadata for Data View
|
|
52
52
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (arrayItemType) fieldMetadata.arrayItemType = arrayItemType;
|
|
71
|
-
const rawRelation = config.rawRelation;
|
|
72
|
-
const isManyToOneRelation = rawRelation && (rawRelation.type === "manyToOne" || rawRelation.type === "n-1" || rawRelation.type === "oneToOne" || rawRelation.type === "1-1") && rawRelation.toward;
|
|
73
|
-
const isOneToOne = rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
|
|
74
|
-
if (isManyToOneRelation && rawRelation.toward) {
|
|
75
|
-
const targetTableName = toCamelCase(rawRelation.toward.type);
|
|
76
|
-
const relationFieldName = rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
|
|
77
|
-
fieldMetadata.relation = {
|
|
78
|
-
fieldName: relationFieldName,
|
|
79
|
-
targetTable: targetTableName
|
|
53
|
+
function dataViewerMetadataGenerator(options = {}) {
|
|
54
|
+
const { distPath = "data-viewer-metadata.generated.ts" } = options;
|
|
55
|
+
return {
|
|
56
|
+
id: "table-metadata",
|
|
57
|
+
description: "Generates table metadata for Data View including field definitions and read permissions",
|
|
58
|
+
dependencies: ["tailordb"],
|
|
59
|
+
processType({ type }) {
|
|
60
|
+
const fields = [];
|
|
61
|
+
const relations = [];
|
|
62
|
+
for (const [fieldName, field] of Object.entries(type.fields)) {
|
|
63
|
+
const config = field.config;
|
|
64
|
+
const { type: fieldType, arrayItemType } = mapFieldType(config.type, config.array);
|
|
65
|
+
const fieldMetadata = {
|
|
66
|
+
name: fieldName,
|
|
67
|
+
type: fieldType,
|
|
68
|
+
required: config.required ?? false,
|
|
69
|
+
description: config.description
|
|
80
70
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
if (config.allowedValues && config.allowedValues.length > 0) fieldMetadata.enumValues = config.allowedValues.map((v) => typeof v === "string" ? v : v.value);
|
|
72
|
+
if (arrayItemType) fieldMetadata.arrayItemType = arrayItemType;
|
|
73
|
+
const rawRelation = config.rawRelation;
|
|
74
|
+
const isManyToOneRelation = rawRelation && (rawRelation.type === "manyToOne" || rawRelation.type === "n-1" || rawRelation.type === "oneToOne" || rawRelation.type === "1-1") && rawRelation.toward;
|
|
75
|
+
const isOneToOne = rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
|
|
76
|
+
if (isManyToOneRelation && rawRelation.toward) {
|
|
77
|
+
const targetTableName = toCamelCase(rawRelation.toward.type);
|
|
78
|
+
const relationFieldName = rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
|
|
79
|
+
fieldMetadata.relation = {
|
|
80
|
+
fieldName: relationFieldName,
|
|
81
|
+
targetTable: targetTableName
|
|
82
|
+
};
|
|
83
|
+
relations.push({
|
|
84
|
+
fieldName: relationFieldName,
|
|
85
|
+
targetTable: targetTableName,
|
|
86
|
+
relationType: "manyToOne",
|
|
87
|
+
foreignKeyField: fieldName,
|
|
88
|
+
backwardFieldName: rawRelation.backward,
|
|
89
|
+
isOneToOne
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
fields.push(fieldMetadata);
|
|
89
93
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
94
|
+
const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
|
|
95
|
+
return {
|
|
96
|
+
name: toCamelCase(type.name),
|
|
97
|
+
pluralForm: toCamelCase(type.pluralForm),
|
|
98
|
+
originalName: type.name,
|
|
99
|
+
description: type.description,
|
|
100
|
+
readAllowedRoles,
|
|
101
|
+
fields,
|
|
102
|
+
relations
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
processResolver() {
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
processExecutor() {
|
|
109
|
+
return null;
|
|
110
|
+
},
|
|
111
|
+
processTailorDBNamespace({ types }) {
|
|
112
|
+
return Object.values(types);
|
|
113
|
+
},
|
|
114
|
+
aggregate({ input }) {
|
|
115
|
+
const allTables = input.tailordb.flatMap((ns) => ns.types);
|
|
116
|
+
const tableByOriginalName = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const table of allTables) tableByOriginalName.set(table.originalName, table);
|
|
118
|
+
for (const table of allTables) for (const relation of table.relations) if (relation.relationType === "manyToOne") {
|
|
119
|
+
if (relation.isOneToOne) continue;
|
|
120
|
+
const targetTable = tableByOriginalName.get(relation.targetTable.charAt(0).toUpperCase() + relation.targetTable.slice(1));
|
|
121
|
+
if (targetTable) {
|
|
122
|
+
const oneToManyFieldName = relation.backwardFieldName ?? table.pluralForm;
|
|
123
|
+
if (!targetTable.relations.some((r) => r.relationType === "oneToMany" && r.fieldName === oneToManyFieldName)) targetTable.relations.push({
|
|
124
|
+
fieldName: oneToManyFieldName,
|
|
125
|
+
targetTable: table.name,
|
|
126
|
+
relationType: "oneToMany",
|
|
127
|
+
foreignKeyField: relation.foreignKeyField
|
|
128
|
+
});
|
|
129
|
+
}
|
|
127
130
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
content: `// This file is auto-generated by table-metadata-generator
|
|
131
|
+
const metadataMap = {};
|
|
132
|
+
for (const table of allTables) {
|
|
133
|
+
const { originalName, ...tableWithoutOriginalName } = table;
|
|
134
|
+
metadataMap[table.name] = tableWithoutOriginalName;
|
|
135
|
+
}
|
|
136
|
+
return { files: [{
|
|
137
|
+
path: distPath,
|
|
138
|
+
content: `// This file is auto-generated by table-metadata-generator
|
|
137
139
|
// Do not edit manually
|
|
138
140
|
|
|
139
141
|
import type {
|
|
@@ -152,10 +154,16 @@ export const tableNames = ${JSON.stringify(Object.keys(metadataMap), null, 2)} a
|
|
|
152
154
|
|
|
153
155
|
export type TableName = (typeof tableNames)[number];
|
|
154
156
|
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
};
|
|
157
|
+
}] };
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Default table metadata generator instance
|
|
163
|
+
* @deprecated Use `dataViewerMetadataGenerator()` instead for custom output path
|
|
164
|
+
*/
|
|
165
|
+
const tableMetadataGenerator = dataViewerMetadataGenerator();
|
|
158
166
|
var metadata_generator_default = tableMetadataGenerator;
|
|
159
167
|
|
|
160
168
|
//#endregion
|
|
161
|
-
export { metadata_generator_default as default, tableMetadataGenerator };
|
|
169
|
+
export { dataViewerMetadataGenerator, metadata_generator_default as default, tableMetadataGenerator };
|
package/package.json
CHANGED
|
@@ -212,6 +212,19 @@ export function useTableData(
|
|
|
212
212
|
queryVariables.query = queryInput;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// Add sort order variable
|
|
216
|
+
if (sortState) {
|
|
217
|
+
queryVariables.order = [
|
|
218
|
+
{ field: sortState.field, direction: sortState.direction },
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add pagination variables
|
|
223
|
+
queryVariables.first = pagination.first;
|
|
224
|
+
if (pagination.after) {
|
|
225
|
+
queryVariables.after = pagination.after;
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
const result = await executeQuery<GraphQLListResponse>(
|
|
216
229
|
client,
|
|
217
230
|
query,
|
package/src/component/index.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
export { DataViewer } from "./data-viewer";
|
|
2
2
|
export type { InitialQuery } from "./data-viewer";
|
|
3
|
-
export { DataViewTabContent } from "./data-view-tab-content";
|
|
4
|
-
export { TableSelector } from "./table-selector";
|
|
5
|
-
export { ColumnSelector } from "./column-selector";
|
|
6
|
-
export { DataTable } from "./data-table";
|
|
7
|
-
export { Pagination } from "./pagination";
|
|
8
|
-
export { SearchFilterForm } from "./search-filter";
|
|
9
|
-
export { ViewSave } from "./view-save-load";
|
|
10
|
-
export { useTableData } from "./hooks/use-table-data";
|
|
11
|
-
export { useColumnState } from "./hooks/use-column-state";
|
|
12
3
|
export { useSavedViews, SavedViewProvider } from "./saved-view-context";
|
|
13
4
|
export type { SavedView, SaveViewInput } from "./saved-view-context";
|
|
14
|
-
export type { SearchFilter, SearchFilters } from "./types";
|
|
@@ -346,8 +346,7 @@ export function SingleRecordTabContent({
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
|
-
|
|
350
|
-
}, [record, tableMetadata.relations]);
|
|
349
|
+
}, [record, tableMetadata.relations, fetchManyToOneData, fetchRelationData]);
|
|
351
350
|
|
|
352
351
|
const handleRefresh = () => {
|
|
353
352
|
fetchRecord();
|
|
@@ -267,174 +267,190 @@ interface GeneratorResult {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
|
-
*
|
|
270
|
+
* Options for the data viewer metadata generator
|
|
271
271
|
*/
|
|
272
|
-
export
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const fields: FieldMetadata[] = [];
|
|
280
|
-
const relations: RelationMetadata[] = [];
|
|
281
|
-
|
|
282
|
-
// Process each field
|
|
283
|
-
for (const [fieldName, field] of Object.entries(type.fields)) {
|
|
284
|
-
const config = field.config;
|
|
285
|
-
|
|
286
|
-
const { type: fieldType, arrayItemType } = mapFieldType(
|
|
287
|
-
config.type,
|
|
288
|
-
config.array,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const fieldMetadata: FieldMetadata = {
|
|
292
|
-
name: fieldName,
|
|
293
|
-
type: fieldType,
|
|
294
|
-
required: config.required ?? false,
|
|
295
|
-
description: config.description,
|
|
296
|
-
};
|
|
272
|
+
export interface DataViewerMetadataGeneratorOptions {
|
|
273
|
+
/**
|
|
274
|
+
* Output file path relative to project root
|
|
275
|
+
* @default "data-viewer-metadata.generated.ts"
|
|
276
|
+
*/
|
|
277
|
+
distPath?: string;
|
|
278
|
+
}
|
|
297
279
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Creates a custom generator that extracts table metadata for Data View
|
|
282
|
+
*/
|
|
283
|
+
export function dataViewerMetadataGenerator(
|
|
284
|
+
options: DataViewerMetadataGeneratorOptions = {},
|
|
285
|
+
) {
|
|
286
|
+
const { distPath = "data-viewer-metadata.generated.ts" } = options;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
id: "table-metadata",
|
|
290
|
+
description:
|
|
291
|
+
"Generates table metadata for Data View including field definitions and read permissions",
|
|
292
|
+
dependencies: ["tailordb"] as ("tailordb" | "resolver" | "executor")[],
|
|
293
|
+
|
|
294
|
+
processType({ type }: { type: ParsedTailorDBType }): ProcessedTable {
|
|
295
|
+
const fields: FieldMetadata[] = [];
|
|
296
|
+
const relations: RelationMetadata[] = [];
|
|
297
|
+
|
|
298
|
+
// Process each field
|
|
299
|
+
for (const [fieldName, field] of Object.entries(type.fields)) {
|
|
300
|
+
const config = field.config;
|
|
301
|
+
|
|
302
|
+
const { type: fieldType, arrayItemType } = mapFieldType(
|
|
303
|
+
config.type,
|
|
304
|
+
config.array,
|
|
302
305
|
);
|
|
303
|
-
}
|
|
304
306
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Extract manyToOne relation info from rawRelation
|
|
311
|
-
// Include: manyToOne, n-1, oneToOne, 1-1 (all are treated as manyToOne for data view purposes)
|
|
312
|
-
const rawRelation = config.rawRelation;
|
|
313
|
-
const isManyToOneRelation =
|
|
314
|
-
rawRelation &&
|
|
315
|
-
(rawRelation.type === "manyToOne" ||
|
|
316
|
-
rawRelation.type === "n-1" ||
|
|
317
|
-
rawRelation.type === "oneToOne" ||
|
|
318
|
-
rawRelation.type === "1-1") &&
|
|
319
|
-
rawRelation.toward;
|
|
320
|
-
|
|
321
|
-
// Check if this is a oneToOne relation (no inverse oneToMany should be generated)
|
|
322
|
-
const isOneToOne =
|
|
323
|
-
rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
|
|
324
|
-
|
|
325
|
-
if (isManyToOneRelation && rawRelation.toward) {
|
|
326
|
-
const targetTableName = toCamelCase(rawRelation.toward.type);
|
|
327
|
-
const relationFieldName =
|
|
328
|
-
rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
|
|
329
|
-
|
|
330
|
-
// Add relation info to the field
|
|
331
|
-
fieldMetadata.relation = {
|
|
332
|
-
fieldName: relationFieldName,
|
|
333
|
-
targetTable: targetTableName,
|
|
307
|
+
const fieldMetadata: FieldMetadata = {
|
|
308
|
+
name: fieldName,
|
|
309
|
+
type: fieldType,
|
|
310
|
+
required: config.required ?? false,
|
|
311
|
+
description: config.description,
|
|
334
312
|
};
|
|
335
313
|
|
|
336
|
-
// Add
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
backwardFieldName: rawRelation.backward,
|
|
343
|
-
isOneToOne,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
314
|
+
// Add enum values if present
|
|
315
|
+
if (config.allowedValues && config.allowedValues.length > 0) {
|
|
316
|
+
fieldMetadata.enumValues = config.allowedValues.map((v) =>
|
|
317
|
+
typeof v === "string" ? v : v.value,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
346
320
|
|
|
347
|
-
|
|
348
|
-
|
|
321
|
+
// Add array item type if it's an array field
|
|
322
|
+
if (arrayItemType) {
|
|
323
|
+
fieldMetadata.arrayItemType = arrayItemType;
|
|
324
|
+
}
|
|
349
325
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
for (const table of allTables) {
|
|
387
|
-
tableByOriginalName.set(table.originalName, table);
|
|
388
|
-
}
|
|
326
|
+
// Extract manyToOne relation info from rawRelation
|
|
327
|
+
// Include: manyToOne, n-1, oneToOne, 1-1 (all are treated as manyToOne for data view purposes)
|
|
328
|
+
const rawRelation = config.rawRelation;
|
|
329
|
+
const isManyToOneRelation =
|
|
330
|
+
rawRelation &&
|
|
331
|
+
(rawRelation.type === "manyToOne" ||
|
|
332
|
+
rawRelation.type === "n-1" ||
|
|
333
|
+
rawRelation.type === "oneToOne" ||
|
|
334
|
+
rawRelation.type === "1-1") &&
|
|
335
|
+
rawRelation.toward;
|
|
336
|
+
|
|
337
|
+
// Check if this is a oneToOne relation (no inverse oneToMany should be generated)
|
|
338
|
+
const isOneToOne =
|
|
339
|
+
rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
|
|
340
|
+
|
|
341
|
+
if (isManyToOneRelation && rawRelation.toward) {
|
|
342
|
+
const targetTableName = toCamelCase(rawRelation.toward.type);
|
|
343
|
+
const relationFieldName =
|
|
344
|
+
rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
|
|
345
|
+
|
|
346
|
+
// Add relation info to the field
|
|
347
|
+
fieldMetadata.relation = {
|
|
348
|
+
fieldName: relationFieldName,
|
|
349
|
+
targetTable: targetTableName,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Add to relations array (include backward field name for oneToMany generation)
|
|
353
|
+
relations.push({
|
|
354
|
+
fieldName: relationFieldName,
|
|
355
|
+
targetTable: targetTableName,
|
|
356
|
+
relationType: "manyToOne",
|
|
357
|
+
foreignKeyField: fieldName,
|
|
358
|
+
backwardFieldName: rawRelation.backward,
|
|
359
|
+
isOneToOne,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
389
362
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
for (const relation of table.relations) {
|
|
393
|
-
if (relation.relationType === "manyToOne") {
|
|
394
|
-
// Skip oneToOne relations - they don't have a oneToMany inverse
|
|
395
|
-
// (GraphQL generates a single object field, not a connection)
|
|
396
|
-
if (relation.isOneToOne) {
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
363
|
+
fields.push(fieldMetadata);
|
|
364
|
+
}
|
|
399
365
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
366
|
+
// Extract read allowed roles from gql permission
|
|
367
|
+
const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
name: toCamelCase(type.name),
|
|
371
|
+
pluralForm: toCamelCase(type.pluralForm),
|
|
372
|
+
originalName: type.name,
|
|
373
|
+
description: type.description,
|
|
374
|
+
readAllowedRoles,
|
|
375
|
+
fields,
|
|
376
|
+
relations,
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
processResolver() {
|
|
381
|
+
return null;
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
processExecutor() {
|
|
385
|
+
return null;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
processTailorDBNamespace({
|
|
389
|
+
types,
|
|
390
|
+
}: {
|
|
391
|
+
types: Record<string, ProcessedTable>;
|
|
392
|
+
}): ProcessedTable[] {
|
|
393
|
+
return Object.values(types);
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
aggregate({ input }: { input: GeneratorInput }): GeneratorResult {
|
|
397
|
+
// Collect all tables from all namespaces
|
|
398
|
+
const allTables = input.tailordb.flatMap((ns) => ns.types);
|
|
399
|
+
|
|
400
|
+
// Build a map of originalName -> table for relation lookup
|
|
401
|
+
const tableByOriginalName = new Map<string, ProcessedTable>();
|
|
402
|
+
for (const table of allTables) {
|
|
403
|
+
tableByOriginalName.set(table.originalName, table);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Second pass: Add oneToMany relations by inverting manyToOne relations
|
|
407
|
+
for (const table of allTables) {
|
|
408
|
+
for (const relation of table.relations) {
|
|
409
|
+
if (relation.relationType === "manyToOne") {
|
|
410
|
+
// Skip oneToOne relations - they don't have a oneToMany inverse
|
|
411
|
+
// (GraphQL generates a single object field, not a connection)
|
|
412
|
+
if (relation.isOneToOne) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Find the target table and add the inverse oneToMany relation
|
|
417
|
+
const targetTable = tableByOriginalName.get(
|
|
418
|
+
// Convert camelCase back to PascalCase for lookup
|
|
419
|
+
relation.targetTable.charAt(0).toUpperCase() +
|
|
420
|
+
relation.targetTable.slice(1),
|
|
414
421
|
);
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
if (targetTable) {
|
|
423
|
+
// Use backward field name if specified, otherwise fall back to plural form
|
|
424
|
+
const oneToManyFieldName =
|
|
425
|
+
relation.backwardFieldName ?? table.pluralForm;
|
|
426
|
+
const alreadyExists = targetTable.relations.some(
|
|
427
|
+
(r) =>
|
|
428
|
+
r.relationType === "oneToMany" &&
|
|
429
|
+
r.fieldName === oneToManyFieldName,
|
|
430
|
+
);
|
|
431
|
+
if (!alreadyExists) {
|
|
432
|
+
targetTable.relations.push({
|
|
433
|
+
fieldName: oneToManyFieldName,
|
|
434
|
+
targetTable: table.name,
|
|
435
|
+
relationType: "oneToMany",
|
|
436
|
+
foreignKeyField: relation.foreignKeyField,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
422
439
|
}
|
|
423
440
|
}
|
|
424
441
|
}
|
|
425
442
|
}
|
|
426
|
-
}
|
|
427
443
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
444
|
+
// Build the metadata map (excluding originalName and backwardFieldName from output)
|
|
445
|
+
const metadataMap: TableMetadataMap = {};
|
|
446
|
+
for (const table of allTables) {
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
448
|
+
const { originalName, ...tableWithoutOriginalName } = table;
|
|
449
|
+
metadataMap[table.name] = tableWithoutOriginalName;
|
|
450
|
+
}
|
|
435
451
|
|
|
436
|
-
|
|
437
|
-
|
|
452
|
+
// Generate the TypeScript content
|
|
453
|
+
const content = `// This file is auto-generated by table-metadata-generator
|
|
438
454
|
// Do not edit manually
|
|
439
455
|
|
|
440
456
|
import type {
|
|
@@ -454,15 +470,22 @@ export const tableNames = ${JSON.stringify(Object.keys(metadataMap), null, 2)} a
|
|
|
454
470
|
export type TableName = (typeof tableNames)[number];
|
|
455
471
|
`;
|
|
456
472
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
};
|
|
473
|
+
return {
|
|
474
|
+
files: [
|
|
475
|
+
{
|
|
476
|
+
path: distPath,
|
|
477
|
+
content,
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Default table metadata generator instance
|
|
487
|
+
* @deprecated Use `dataViewerMetadataGenerator()` instead for custom output path
|
|
488
|
+
*/
|
|
489
|
+
export const tableMetadataGenerator = dataViewerMetadataGenerator();
|
|
467
490
|
|
|
468
491
|
export default tableMetadataGenerator;
|