@izumisy-tailor/tailor-data-viewer 0.1.9 → 0.1.11

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 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 tableMetadataGenerator from "@izumisy-tailor/tailor-data-viewer/generator";
117
+ import { dataViewerMetadataGenerator } from "@izumisy-tailor/tailor-data-viewer/generator";
132
118
 
133
119
  export const generators = defineGenerators(
134
- tableMetadataGenerator,
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 `src/generated/table-metadata.ts` file containing type-safe metadata for all your TailorDB types, including fields, relations, and role-based access control settings.
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
- * Custom generator that extracts table metadata for Data View
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 };
@@ -48,92 +48,94 @@ function extractReadAllowedRoles(gqlPermission) {
48
48
  return Array.from(roles);
49
49
  }
50
50
  /**
51
- * Custom generator that extracts table metadata for Data View
51
+ * Creates a custom generator that extracts table metadata for Data View
52
52
  */
53
- const tableMetadataGenerator = {
54
- id: "table-metadata",
55
- description: "Generates table metadata for Data View including field definitions and read permissions",
56
- dependencies: ["tailordb"],
57
- processType({ type }) {
58
- const fields = [];
59
- const relations = [];
60
- for (const [fieldName, field] of Object.entries(type.fields)) {
61
- const config = field.config;
62
- const { type: fieldType, arrayItemType } = mapFieldType(config.type, config.array);
63
- const fieldMetadata = {
64
- name: fieldName,
65
- type: fieldType,
66
- required: config.required ?? false,
67
- description: config.description
68
- };
69
- if (config.allowedValues && config.allowedValues.length > 0) fieldMetadata.enumValues = config.allowedValues.map((v) => typeof v === "string" ? v : v.value);
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
- relations.push({
82
- fieldName: relationFieldName,
83
- targetTable: targetTableName,
84
- relationType: "manyToOne",
85
- foreignKeyField: fieldName,
86
- backwardFieldName: rawRelation.backward,
87
- isOneToOne
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
- fields.push(fieldMetadata);
91
- }
92
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
93
- return {
94
- name: toCamelCase(type.name),
95
- pluralForm: toCamelCase(type.pluralForm),
96
- originalName: type.name,
97
- description: type.description,
98
- readAllowedRoles,
99
- fields,
100
- relations
101
- };
102
- },
103
- processResolver() {
104
- return null;
105
- },
106
- processExecutor() {
107
- return null;
108
- },
109
- processTailorDBNamespace({ types }) {
110
- return Object.values(types);
111
- },
112
- aggregate({ input }) {
113
- const allTables = input.tailordb.flatMap((ns) => ns.types);
114
- const tableByOriginalName = /* @__PURE__ */ new Map();
115
- for (const table of allTables) tableByOriginalName.set(table.originalName, table);
116
- for (const table of allTables) for (const relation of table.relations) if (relation.relationType === "manyToOne") {
117
- if (relation.isOneToOne) continue;
118
- const targetTable = tableByOriginalName.get(relation.targetTable.charAt(0).toUpperCase() + relation.targetTable.slice(1));
119
- if (targetTable) {
120
- const oneToManyFieldName = relation.backwardFieldName ?? table.pluralForm;
121
- if (!targetTable.relations.some((r) => r.relationType === "oneToMany" && r.fieldName === oneToManyFieldName)) targetTable.relations.push({
122
- fieldName: oneToManyFieldName,
123
- targetTable: table.name,
124
- relationType: "oneToMany",
125
- foreignKeyField: relation.foreignKeyField
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
- const metadataMap = {};
130
- for (const table of allTables) {
131
- const { originalName, ...tableWithoutOriginalName } = table;
132
- metadataMap[table.name] = tableWithoutOriginalName;
133
- }
134
- return { files: [{
135
- path: "src/generated/table-metadata.ts",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.1.9",
4
+ "version": "0.1.11",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from "react";
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
2
  import { Plus, X } from "lucide-react";
3
3
  import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4
4
  import { Button } from "./ui/button";
@@ -57,27 +57,13 @@ export function DataViewer({
57
57
  initialViewId,
58
58
  }: DataViewerProps) {
59
59
  const allTables = Object.values(tableMetadata);
60
- const { getViewById } = useSavedViews();
60
+ const { getViewById, isLoading } = useSavedViews();
61
61
 
62
- // Tab management - initialize with saved view if provided
62
+ // Track if we've already initialized from the saved view
63
+ const initializedFromViewRef = useRef(false);
64
+
65
+ // Tab management - start with empty tab, will apply saved view after loading
63
66
  const [tabs, setTabs] = useState<Tab[]>(() => {
64
- if (initialViewId) {
65
- const view = getViewById(initialViewId);
66
- if (view) {
67
- const table = tableMetadata[view.tableName];
68
- if (table) {
69
- return [
70
- {
71
- id: generateTabId(),
72
- label: view.name,
73
- table,
74
- isLocked: true,
75
- initialView: view,
76
- },
77
- ];
78
- }
79
- }
80
- }
81
67
  return [
82
68
  {
83
69
  id: generateTabId(),
@@ -89,6 +75,31 @@ export function DataViewer({
89
75
  });
90
76
  const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);
91
77
 
78
+ // Apply saved view after views are loaded from store
79
+ useEffect(() => {
80
+ // Skip if no initialViewId, already initialized, or still loading
81
+ if (!initialViewId || initializedFromViewRef.current || isLoading) {
82
+ return;
83
+ }
84
+
85
+ const view = getViewById(initialViewId);
86
+ if (view) {
87
+ const table = tableMetadata[view.tableName];
88
+ if (table) {
89
+ initializedFromViewRef.current = true;
90
+ const newTab: Tab = {
91
+ id: generateTabId(),
92
+ label: view.name,
93
+ table,
94
+ isLocked: true,
95
+ initialView: view,
96
+ };
97
+ setTabs([newTab]);
98
+ setActiveTabId(newTab.id);
99
+ }
100
+ }
101
+ }, [initialViewId, isLoading, getViewById, tableMetadata]);
102
+
92
103
  const handleAddTab = useCallback(() => {
93
104
  const newTab: Tab = {
94
105
  id: generateTabId(),
@@ -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
- // eslint-disable-next-line react-hooks/exhaustive-deps
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
- * Custom generator that extracts table metadata for Data View
270
+ * Options for the data viewer metadata generator
271
271
  */
272
- export const tableMetadataGenerator = {
273
- id: "table-metadata",
274
- description:
275
- "Generates table metadata for Data View including field definitions and read permissions",
276
- dependencies: ["tailordb"] as ("tailordb" | "resolver" | "executor")[],
277
-
278
- processType({ type }: { type: ParsedTailorDBType }): ProcessedTable {
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
- // Add enum values if present
299
- if (config.allowedValues && config.allowedValues.length > 0) {
300
- fieldMetadata.enumValues = config.allowedValues.map((v) =>
301
- typeof v === "string" ? v : v.value,
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
- // Add array item type if it's an array field
306
- if (arrayItemType) {
307
- fieldMetadata.arrayItemType = arrayItemType;
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 to relations array (include backward field name for oneToMany generation)
337
- relations.push({
338
- fieldName: relationFieldName,
339
- targetTable: targetTableName,
340
- relationType: "manyToOne",
341
- foreignKeyField: fieldName,
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
- fields.push(fieldMetadata);
348
- }
321
+ // Add array item type if it's an array field
322
+ if (arrayItemType) {
323
+ fieldMetadata.arrayItemType = arrayItemType;
324
+ }
349
325
 
350
- // Extract read allowed roles from gql permission
351
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
352
-
353
- return {
354
- name: toCamelCase(type.name),
355
- pluralForm: toCamelCase(type.pluralForm),
356
- originalName: type.name,
357
- description: type.description,
358
- readAllowedRoles,
359
- fields,
360
- relations,
361
- };
362
- },
363
-
364
- processResolver() {
365
- return null;
366
- },
367
-
368
- processExecutor() {
369
- return null;
370
- },
371
-
372
- processTailorDBNamespace({
373
- types,
374
- }: {
375
- types: Record<string, ProcessedTable>;
376
- }): ProcessedTable[] {
377
- return Object.values(types);
378
- },
379
-
380
- aggregate({ input }: { input: GeneratorInput }): GeneratorResult {
381
- // Collect all tables from all namespaces
382
- const allTables = input.tailordb.flatMap((ns) => ns.types);
383
-
384
- // Build a map of originalName -> table for relation lookup
385
- const tableByOriginalName = new Map<string, ProcessedTable>();
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
- // Second pass: Add oneToMany relations by inverting manyToOne relations
391
- for (const table of allTables) {
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
- // Find the target table and add the inverse oneToMany relation
401
- const targetTable = tableByOriginalName.get(
402
- // Convert camelCase back to PascalCase for lookup
403
- relation.targetTable.charAt(0).toUpperCase() +
404
- relation.targetTable.slice(1),
405
- );
406
- if (targetTable) {
407
- // Use backward field name if specified, otherwise fall back to plural form
408
- const oneToManyFieldName =
409
- relation.backwardFieldName ?? table.pluralForm;
410
- const alreadyExists = targetTable.relations.some(
411
- (r) =>
412
- r.relationType === "oneToMany" &&
413
- r.fieldName === oneToManyFieldName,
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 (!alreadyExists) {
416
- targetTable.relations.push({
417
- fieldName: oneToManyFieldName,
418
- targetTable: table.name,
419
- relationType: "oneToMany",
420
- foreignKeyField: relation.foreignKeyField,
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
- // Build the metadata map (excluding originalName and backwardFieldName from output)
429
- const metadataMap: TableMetadataMap = {};
430
- for (const table of allTables) {
431
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
432
- const { originalName, ...tableWithoutOriginalName } = table;
433
- metadataMap[table.name] = tableWithoutOriginalName;
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
- // Generate the TypeScript content
437
- const content = `// This file is auto-generated by table-metadata-generator
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
- return {
458
- files: [
459
- {
460
- path: "src/generated/table-metadata.ts",
461
- content,
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;