@izumisy-tailor/tailor-data-viewer 0.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.
Files changed (37) hide show
  1. package/README.md +255 -0
  2. package/package.json +47 -0
  3. package/src/component/column-selector.tsx +264 -0
  4. package/src/component/data-table.tsx +428 -0
  5. package/src/component/data-view-tab-content.tsx +324 -0
  6. package/src/component/data-viewer.tsx +280 -0
  7. package/src/component/hooks/use-accessible-tables.ts +22 -0
  8. package/src/component/hooks/use-column-state.ts +281 -0
  9. package/src/component/hooks/use-relation-data.ts +387 -0
  10. package/src/component/hooks/use-table-data.ts +317 -0
  11. package/src/component/index.ts +15 -0
  12. package/src/component/pagination.tsx +56 -0
  13. package/src/component/relation-content.tsx +250 -0
  14. package/src/component/saved-view-context.tsx +145 -0
  15. package/src/component/search-filter.tsx +319 -0
  16. package/src/component/single-record-tab-content.tsx +676 -0
  17. package/src/component/table-selector.tsx +102 -0
  18. package/src/component/types.ts +20 -0
  19. package/src/component/view-save-load.tsx +112 -0
  20. package/src/generator/metadata-generator.ts +461 -0
  21. package/src/lib/utils.ts +6 -0
  22. package/src/providers/graphql-client.ts +31 -0
  23. package/src/styles/theme.css +105 -0
  24. package/src/types/table-metadata.ts +73 -0
  25. package/src/ui/alert.tsx +66 -0
  26. package/src/ui/badge.tsx +46 -0
  27. package/src/ui/button.tsx +62 -0
  28. package/src/ui/card.tsx +92 -0
  29. package/src/ui/checkbox.tsx +30 -0
  30. package/src/ui/collapsible.tsx +31 -0
  31. package/src/ui/dialog.tsx +143 -0
  32. package/src/ui/dropdown-menu.tsx +255 -0
  33. package/src/ui/input.tsx +21 -0
  34. package/src/ui/label.tsx +24 -0
  35. package/src/ui/select.tsx +188 -0
  36. package/src/ui/table.tsx +116 -0
  37. package/src/utils/query-builder.ts +190 -0
@@ -0,0 +1,102 @@
1
+ import { Database } from "lucide-react";
2
+ import type { TableMetadata } from "../types/table-metadata";
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "../ui/select";
10
+
11
+ interface TableSelectorProps {
12
+ tables: TableMetadata[];
13
+ selectedTable: TableMetadata | null;
14
+ onSelect: (table: TableMetadata) => void;
15
+ centered?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Dropdown selector for choosing a table to view
20
+ */
21
+ export function TableSelector({
22
+ tables,
23
+ selectedTable,
24
+ onSelect,
25
+ centered = false,
26
+ }: TableSelectorProps) {
27
+ const handleValueChange = (tableName: string) => {
28
+ const table = tables.find((t) => t.name === tableName);
29
+ if (table) {
30
+ onSelect(table);
31
+ }
32
+ };
33
+
34
+ // Empty state: show centered dropdown with icon and description
35
+ if (centered) {
36
+ return (
37
+ <div className="flex flex-col items-center space-y-4">
38
+ <div className="text-center">
39
+ <Database className="text-muted-foreground mx-auto mb-2 h-10 w-10" />
40
+ <h3 className="text-lg font-semibold">テーブルを選択</h3>
41
+ <p className="text-muted-foreground text-sm">
42
+ 閲覧するテーブルを選択してください
43
+ </p>
44
+ </div>
45
+ <Select
46
+ value={selectedTable?.name ?? ""}
47
+ onValueChange={handleValueChange}
48
+ >
49
+ <SelectTrigger className="w-[400px]">
50
+ <SelectValue placeholder="テーブルを選択..." />
51
+ </SelectTrigger>
52
+ <SelectContent className="max-h-[400px]">
53
+ {tables.map((table) => (
54
+ <SelectItem
55
+ key={table.name}
56
+ value={table.name}
57
+ className="flex flex-col items-start py-2"
58
+ >
59
+ <div className="flex w-full flex-col gap-0.5">
60
+ <span className="font-medium">{table.name}</span>
61
+ {table.description && (
62
+ <span className="text-muted-foreground text-xs">
63
+ {table.description}
64
+ </span>
65
+ )}
66
+ </div>
67
+ </SelectItem>
68
+ ))}
69
+ </SelectContent>
70
+ </Select>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className="flex items-center gap-2">
77
+ <label className="text-muted-foreground text-sm font-medium">
78
+ テーブル:
79
+ </label>
80
+ <Select
81
+ value={selectedTable?.name ?? ""}
82
+ onValueChange={handleValueChange}
83
+ >
84
+ <SelectTrigger className="w-[250px]">
85
+ <SelectValue placeholder="テーブルを選択..." />
86
+ </SelectTrigger>
87
+ <SelectContent>
88
+ {tables.map((table) => (
89
+ <SelectItem key={table.name} value={table.name}>
90
+ {table.name}
91
+ {table.description && (
92
+ <span className="text-muted-foreground ml-2 text-xs">
93
+ ({table.description})
94
+ </span>
95
+ )}
96
+ </SelectItem>
97
+ ))}
98
+ </SelectContent>
99
+ </Select>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,20 @@
1
+ import type { FieldType } from "../types/table-metadata";
2
+
3
+ /**
4
+ * Search filter condition for a single field
5
+ */
6
+ export interface SearchFilter {
7
+ /** Field name to filter */
8
+ field: string;
9
+ /** Field type (determines UI input type and query format) */
10
+ fieldType: FieldType;
11
+ /** Filter value (string for string/number/enum, boolean for boolean) */
12
+ value: string | boolean;
13
+ /** Enum values (if fieldType is "enum") */
14
+ enumValues?: readonly string[];
15
+ }
16
+
17
+ /**
18
+ * Search filters state - a collection of filters to be applied with AND logic
19
+ */
20
+ export type SearchFilters = SearchFilter[];
@@ -0,0 +1,112 @@
1
+ import { useState, useCallback } from "react";
2
+ import { Save, Filter, Columns } from "lucide-react";
3
+ import { Button } from "../ui/button";
4
+ import { Input } from "../ui/input";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from "../ui/dialog";
14
+ import type { ExpandedRelationFields } from "../types/table-metadata";
15
+ import { useSavedViews, type SaveViewInput } from "./saved-view-context";
16
+ import type { SearchFilters } from "./types";
17
+
18
+ interface ViewSaveProps {
19
+ tableName: string;
20
+ filters: SearchFilters;
21
+ selectedFields: string[];
22
+ selectedRelations: string[];
23
+ expandedRelationFields: ExpandedRelationFields;
24
+ }
25
+
26
+ /**
27
+ * View save control
28
+ * Allows saving views (filters + column selections)
29
+ */
30
+ export function ViewSave({
31
+ tableName,
32
+ filters,
33
+ selectedFields,
34
+ selectedRelations,
35
+ expandedRelationFields,
36
+ }: ViewSaveProps) {
37
+ const [saveDialogOpen, setSaveDialogOpen] = useState(false);
38
+ const [viewName, setViewName] = useState("");
39
+
40
+ const { saveView } = useSavedViews();
41
+
42
+ const handleSaveView = useCallback(() => {
43
+ if (!viewName.trim()) return;
44
+
45
+ const input: SaveViewInput = {
46
+ name: viewName.trim(),
47
+ tableName,
48
+ filters,
49
+ selectedFields,
50
+ selectedRelations,
51
+ expandedRelationFields,
52
+ };
53
+ saveView(input);
54
+ setViewName("");
55
+ setSaveDialogOpen(false);
56
+ }, [
57
+ viewName,
58
+ tableName,
59
+ filters,
60
+ selectedFields,
61
+ selectedRelations,
62
+ expandedRelationFields,
63
+ saveView,
64
+ ]);
65
+
66
+ const canSave = filters.length > 0 || selectedFields.length > 0;
67
+
68
+ return (
69
+ <Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
70
+ <DialogTrigger asChild>
71
+ <Button variant="outline" size="sm" disabled={!canSave}>
72
+ <Save className="mr-1 size-3" />
73
+ ビュー保存
74
+ </Button>
75
+ </DialogTrigger>
76
+ <DialogContent>
77
+ <DialogHeader>
78
+ <DialogTitle>ビューを保存</DialogTitle>
79
+ <DialogDescription>
80
+ 現在の検索条件とカラム選択に名前を付けて保存します。
81
+ </DialogDescription>
82
+ </DialogHeader>
83
+ <div className="space-y-4 py-4">
84
+ <Input
85
+ placeholder="ビューの名前"
86
+ value={viewName}
87
+ onChange={(e) => setViewName(e.target.value)}
88
+ />
89
+ <div className="text-muted-foreground space-y-1 text-sm">
90
+ <div>テーブル: {tableName}</div>
91
+ <div className="flex items-center gap-1">
92
+ <Filter className="size-3" />
93
+ フィルター数: {filters.length}
94
+ </div>
95
+ <div className="flex items-center gap-1">
96
+ <Columns className="size-3" />
97
+ 選択カラム数: {selectedFields.length}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <DialogFooter>
102
+ <Button variant="outline" onClick={() => setSaveDialogOpen(false)}>
103
+ キャンセル
104
+ </Button>
105
+ <Button onClick={handleSaveView} disabled={!viewName.trim()}>
106
+ 保存
107
+ </Button>
108
+ </DialogFooter>
109
+ </DialogContent>
110
+ </Dialog>
111
+ );
112
+ }
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Field type mapping for Data View
3
+ */
4
+ export type FieldType =
5
+ | "string"
6
+ | "number"
7
+ | "boolean"
8
+ | "uuid"
9
+ | "datetime"
10
+ | "date"
11
+ | "time"
12
+ | "enum"
13
+ | "array"
14
+ | "nested";
15
+
16
+ /**
17
+ * Metadata for a single field
18
+ */
19
+ export interface FieldMetadata {
20
+ name: string;
21
+ type: FieldType;
22
+ required: boolean;
23
+ enumValues?: readonly string[];
24
+ arrayItemType?: FieldType;
25
+ description?: string;
26
+ /** manyToOne relation info (if this field is a foreign key) */
27
+ relation?: {
28
+ /** GraphQL field name for the related object (e.g., "task") */
29
+ fieldName: string;
30
+ /** Target table name in camelCase (e.g., "task") */
31
+ targetTable: string;
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Metadata for a relation
37
+ */
38
+ export interface RelationMetadata {
39
+ /** GraphQL field name (e.g., "task" for manyToOne, "taskAssignments" for oneToMany) */
40
+ fieldName: string;
41
+ /** Target table name in camelCase (e.g., "task", "taskAssignment") */
42
+ targetTable: string;
43
+ /** Relation type */
44
+ relationType: "manyToOne" | "oneToMany";
45
+ /** For manyToOne: the FK field name (e.g., "taskId"). For oneToMany: the FK field on the child table */
46
+ foreignKeyField: string;
47
+ /** For manyToOne: the backward field name on the target type (used to generate oneToMany relation) */
48
+ backwardFieldName?: string;
49
+ /** True if this is a oneToOne relation (no inverse oneToMany should be generated) */
50
+ isOneToOne?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Metadata for a single table
55
+ */
56
+ export interface TableMetadata {
57
+ name: string;
58
+ pluralForm: string;
59
+ description?: string;
60
+ readAllowedRoles: string[];
61
+ fields: FieldMetadata[];
62
+ /** Relations (manyToOne and oneToMany) */
63
+ relations?: RelationMetadata[];
64
+ }
65
+
66
+ /**
67
+ * Map of all tables
68
+ */
69
+ export type TableMetadataMap = Record<string, TableMetadata>;
70
+
71
+ /**
72
+ * Intermediate type for processed table data
73
+ */
74
+ interface ProcessedTable {
75
+ name: string;
76
+ pluralForm: string;
77
+ originalName: string; // PascalCase name for relation lookup
78
+ description?: string;
79
+ readAllowedRoles: string[];
80
+ fields: FieldMetadata[];
81
+ relations: RelationMetadata[];
82
+ }
83
+
84
+ /**
85
+ * Map TailorDB field type to FieldType
86
+ */
87
+ function mapFieldType(
88
+ tailorType: string,
89
+ isArray?: boolean,
90
+ ): { type: FieldType; arrayItemType?: FieldType } {
91
+ const typeMap: Record<string, FieldType> = {
92
+ string: "string",
93
+ integer: "number",
94
+ float: "number",
95
+ boolean: "boolean",
96
+ uuid: "uuid",
97
+ datetime: "datetime",
98
+ date: "date",
99
+ time: "time",
100
+ enum: "enum",
101
+ nested: "nested",
102
+ };
103
+
104
+ const mappedType = typeMap[tailorType] ?? "string";
105
+
106
+ if (isArray) {
107
+ return { type: "array", arrayItemType: mappedType };
108
+ }
109
+
110
+ return { type: mappedType };
111
+ }
112
+
113
+ /**
114
+ * Convert PascalCase to camelCase
115
+ */
116
+ function toCamelCase(str: string): string {
117
+ return str.charAt(0).toLowerCase() + str.slice(1);
118
+ }
119
+
120
+ /**
121
+ * Extract allowed roles from gql permission policies
122
+ * Only extracts roles from 'read' action policies with 'allow' permit
123
+ */
124
+ function extractReadAllowedRoles(
125
+ gqlPermission?: readonly {
126
+ conditions: readonly unknown[];
127
+ actions: readonly ["all"] | readonly string[];
128
+ permit: "allow" | "deny";
129
+ description?: string;
130
+ }[],
131
+ ): string[] {
132
+ if (!gqlPermission) return [];
133
+
134
+ const roles = new Set<string>();
135
+
136
+ for (const policy of gqlPermission) {
137
+ // Only process 'allow' policies that include 'read' action
138
+ if (policy.permit !== "allow") continue;
139
+ const actions = policy.actions as readonly string[];
140
+ if (!actions.includes("all") && !actions.includes("read")) continue;
141
+
142
+ // Extract roles from conditions
143
+ for (const condition of policy.conditions) {
144
+ if (!Array.isArray(condition) || condition.length < 3) continue;
145
+
146
+ const [left, operator, right] = condition;
147
+
148
+ // Check for pattern: ["ROLE_NAME", "in", { user: "roles" }]
149
+ if (
150
+ typeof left === "string" &&
151
+ operator === "in" &&
152
+ typeof right === "object" &&
153
+ right !== null &&
154
+ "user" in right &&
155
+ (right as { user: string }).user === "roles"
156
+ ) {
157
+ roles.add(left);
158
+ }
159
+
160
+ // Check for pattern: [{ user: "roles" }, "in", "ROLE_NAME"] (reversed)
161
+ if (
162
+ typeof right === "string" &&
163
+ operator === "in" &&
164
+ typeof left === "object" &&
165
+ left !== null &&
166
+ "user" in left &&
167
+ (left as { user: string }).user === "roles"
168
+ ) {
169
+ roles.add(right);
170
+ }
171
+ }
172
+ }
173
+
174
+ return Array.from(roles);
175
+ }
176
+
177
+ /**
178
+ * Parsed field type from TailorDB
179
+ */
180
+ interface ParsedFieldConfig {
181
+ type: string;
182
+ required?: boolean;
183
+ description?: string;
184
+ allowedValues?: ({ value: string } | string)[];
185
+ array?: boolean;
186
+ /** Raw relation configuration from TailorDB SDK */
187
+ rawRelation?: {
188
+ type: "manyToOne" | "n-1" | "oneToOne" | "1-1" | "keyOnly";
189
+ toward: {
190
+ /** Target type name (PascalCase) */
191
+ type: string;
192
+ /** GraphQL field alias for the relation */
193
+ as?: string;
194
+ };
195
+ /** Backward relation field name on the target type */
196
+ backward?: string;
197
+ };
198
+ /** Foreign key info */
199
+ foreignKey?: boolean;
200
+ foreignKeyType?: string;
201
+ foreignKeyField?: string;
202
+ }
203
+
204
+ /**
205
+ * Parsed field from TailorDB
206
+ */
207
+ interface ParsedField {
208
+ name: string;
209
+ config: ParsedFieldConfig;
210
+ }
211
+
212
+ /**
213
+ * Parsed TailorDB type
214
+ */
215
+ interface ParsedTailorDBType {
216
+ name: string;
217
+ pluralForm: string;
218
+ description?: string;
219
+ fields: Record<string, ParsedField>;
220
+ permissions: {
221
+ gql?: readonly GqlPermissionPolicy[];
222
+ };
223
+ }
224
+
225
+ /**
226
+ * GQL permission policy
227
+ */
228
+ interface GqlPermissionPolicy {
229
+ conditions: readonly unknown[];
230
+ actions: readonly ["all"] | readonly string[];
231
+ permit: "allow" | "deny";
232
+ description?: string;
233
+ }
234
+
235
+ /**
236
+ * Generator input structure
237
+ */
238
+ interface GeneratorInput {
239
+ tailordb: {
240
+ types: ProcessedTable[];
241
+ }[];
242
+ }
243
+
244
+ /**
245
+ * Generator result file
246
+ */
247
+ interface GeneratorResultFile {
248
+ path: string;
249
+ content: string;
250
+ skipIfExists?: boolean;
251
+ executable?: boolean;
252
+ }
253
+
254
+ /**
255
+ * Generator result
256
+ */
257
+ interface GeneratorResult {
258
+ files: GeneratorResultFile[];
259
+ errors?: string[];
260
+ }
261
+
262
+ /**
263
+ * Custom generator that extracts table metadata for Data View
264
+ */
265
+ export const tableMetadataGenerator = {
266
+ id: "table-metadata",
267
+ description:
268
+ "Generates table metadata for Data View including field definitions and read permissions",
269
+ dependencies: ["tailordb"] as ("tailordb" | "resolver" | "executor")[],
270
+
271
+ processType({ type }: { type: ParsedTailorDBType }): ProcessedTable {
272
+ const fields: FieldMetadata[] = [];
273
+ const relations: RelationMetadata[] = [];
274
+
275
+ // Process each field
276
+ for (const [fieldName, field] of Object.entries(type.fields)) {
277
+ const config = field.config;
278
+
279
+ const { type: fieldType, arrayItemType } = mapFieldType(
280
+ config.type,
281
+ config.array,
282
+ );
283
+
284
+ const fieldMetadata: FieldMetadata = {
285
+ name: fieldName,
286
+ type: fieldType,
287
+ required: config.required ?? false,
288
+ description: config.description,
289
+ };
290
+
291
+ // Add enum values if present
292
+ if (config.allowedValues && config.allowedValues.length > 0) {
293
+ fieldMetadata.enumValues = config.allowedValues.map((v) =>
294
+ typeof v === "string" ? v : v.value,
295
+ );
296
+ }
297
+
298
+ // Add array item type if it's an array field
299
+ if (arrayItemType) {
300
+ fieldMetadata.arrayItemType = arrayItemType;
301
+ }
302
+
303
+ // Extract manyToOne relation info from rawRelation
304
+ // Include: manyToOne, n-1, oneToOne, 1-1 (all are treated as manyToOne for data view purposes)
305
+ const rawRelation = config.rawRelation;
306
+ const isManyToOneRelation =
307
+ rawRelation &&
308
+ (rawRelation.type === "manyToOne" ||
309
+ rawRelation.type === "n-1" ||
310
+ rawRelation.type === "oneToOne" ||
311
+ rawRelation.type === "1-1") &&
312
+ rawRelation.toward;
313
+
314
+ // Check if this is a oneToOne relation (no inverse oneToMany should be generated)
315
+ const isOneToOne =
316
+ rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
317
+
318
+ if (isManyToOneRelation && rawRelation.toward) {
319
+ const targetTableName = toCamelCase(rawRelation.toward.type);
320
+ const relationFieldName =
321
+ rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
322
+
323
+ // Add relation info to the field
324
+ fieldMetadata.relation = {
325
+ fieldName: relationFieldName,
326
+ targetTable: targetTableName,
327
+ };
328
+
329
+ // Add to relations array (include backward field name for oneToMany generation)
330
+ relations.push({
331
+ fieldName: relationFieldName,
332
+ targetTable: targetTableName,
333
+ relationType: "manyToOne",
334
+ foreignKeyField: fieldName,
335
+ backwardFieldName: rawRelation.backward,
336
+ isOneToOne,
337
+ });
338
+ }
339
+
340
+ fields.push(fieldMetadata);
341
+ }
342
+
343
+ // Extract read allowed roles from gql permission
344
+ const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
345
+
346
+ return {
347
+ name: toCamelCase(type.name),
348
+ pluralForm: toCamelCase(type.pluralForm),
349
+ originalName: type.name,
350
+ description: type.description,
351
+ readAllowedRoles,
352
+ fields,
353
+ relations,
354
+ };
355
+ },
356
+
357
+ processResolver() {
358
+ return null;
359
+ },
360
+
361
+ processExecutor() {
362
+ return null;
363
+ },
364
+
365
+ processTailorDBNamespace({
366
+ types,
367
+ }: {
368
+ types: Record<string, ProcessedTable>;
369
+ }): ProcessedTable[] {
370
+ return Object.values(types);
371
+ },
372
+
373
+ aggregate({ input }: { input: GeneratorInput }): GeneratorResult {
374
+ // Collect all tables from all namespaces
375
+ const allTables = input.tailordb.flatMap((ns) => ns.types);
376
+
377
+ // Build a map of originalName -> table for relation lookup
378
+ const tableByOriginalName = new Map<string, ProcessedTable>();
379
+ for (const table of allTables) {
380
+ tableByOriginalName.set(table.originalName, table);
381
+ }
382
+
383
+ // Second pass: Add oneToMany relations by inverting manyToOne relations
384
+ for (const table of allTables) {
385
+ for (const relation of table.relations) {
386
+ if (relation.relationType === "manyToOne") {
387
+ // Skip oneToOne relations - they don't have a oneToMany inverse
388
+ // (GraphQL generates a single object field, not a connection)
389
+ if (relation.isOneToOne) {
390
+ continue;
391
+ }
392
+
393
+ // Find the target table and add the inverse oneToMany relation
394
+ const targetTable = tableByOriginalName.get(
395
+ // Convert camelCase back to PascalCase for lookup
396
+ relation.targetTable.charAt(0).toUpperCase() +
397
+ relation.targetTable.slice(1),
398
+ );
399
+ if (targetTable) {
400
+ // Use backward field name if specified, otherwise fall back to plural form
401
+ const oneToManyFieldName =
402
+ relation.backwardFieldName ?? table.pluralForm;
403
+ const alreadyExists = targetTable.relations.some(
404
+ (r) =>
405
+ r.relationType === "oneToMany" &&
406
+ r.fieldName === oneToManyFieldName,
407
+ );
408
+ if (!alreadyExists) {
409
+ targetTable.relations.push({
410
+ fieldName: oneToManyFieldName,
411
+ targetTable: table.name,
412
+ relationType: "oneToMany",
413
+ foreignKeyField: relation.foreignKeyField,
414
+ });
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ // Build the metadata map (excluding originalName and backwardFieldName from output)
422
+ const metadataMap: TableMetadataMap = {};
423
+ for (const table of allTables) {
424
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
425
+ const { originalName, ...tableWithoutOriginalName } = table;
426
+ metadataMap[table.name] = tableWithoutOriginalName;
427
+ }
428
+
429
+ // Generate the TypeScript content
430
+ const content = `// This file is auto-generated by table-metadata-generator
431
+ // Do not edit manually
432
+
433
+ import type {
434
+ FieldType,
435
+ FieldMetadata,
436
+ RelationMetadata,
437
+ TableMetadata,
438
+ TableMetadataMap,
439
+ } from "../../generator/table-metadata-generator";
440
+
441
+ export type { FieldType, FieldMetadata, RelationMetadata, TableMetadata, TableMetadataMap };
442
+
443
+ export const tableMetadata: TableMetadataMap = ${JSON.stringify(metadataMap, null, 2)} as const;
444
+
445
+ export const tableNames = ${JSON.stringify(Object.keys(metadataMap), null, 2)} as const;
446
+
447
+ export type TableName = (typeof tableNames)[number];
448
+ `;
449
+
450
+ return {
451
+ files: [
452
+ {
453
+ path: "src/generated/table-metadata.ts",
454
+ content,
455
+ },
456
+ ],
457
+ };
458
+ },
459
+ };
460
+
461
+ export default tableMetadataGenerator;
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }