@ghentcdh/crouton-cli 0.0.1-alpha.81 → 0.0.1-alpha.89
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 +87 -0
- package/agent-files/.claude/skills/crouton/SKILL.md +59 -0
- package/agent-files/.claude/skills/crouton/references/common-mistakes.md +89 -0
- package/agent-files/.claude/skills/crouton/references/data-sources.md +47 -0
- package/agent-files/.claude/skills/crouton/references/field-inputs.md +52 -0
- package/agent-files/.claude/skills/crouton/references/recipes.md +222 -0
- package/agent-files/.claude/skills/crouton/references/relations.md +150 -0
- package/agent-files/.claude/skills/crouton/references/resource-json.md +326 -0
- package/agent-files/.cursor/rules/crouton.mdc +39 -0
- package/{create-datasource → dist/create-datasource}/index.js +7440 -4094
- package/dist/create-datasource/index.js.map +1 -0
- package/{index.js → dist/index.js} +10172 -6341
- package/dist/index.js.map +1 -0
- package/dist/package.json +22 -0
- package/package.json +38 -8
- package/create-datasource/index.js.map +0 -1
- package/index.js.map +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Relations — recipes
|
|
2
|
+
|
|
3
|
+
Relations are configured on the **column** via `column.relation`, not inside `fieldInput.options`.
|
|
4
|
+
`fieldInput` only picks and configures the UI widget.
|
|
5
|
+
|
|
6
|
+
## manyToOne (autocomplete lookup)
|
|
7
|
+
|
|
8
|
+
Use when: this resource has a FK column pointing to another resource (e.g. `book.authorId → author.id`).
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"columns": {
|
|
13
|
+
"authorId": {
|
|
14
|
+
"label": "Author",
|
|
15
|
+
"relation": {
|
|
16
|
+
"resource": "./author/resource.json",
|
|
17
|
+
"type": "manyToOne",
|
|
18
|
+
"foreignKey": "authorId",
|
|
19
|
+
"displayKey": "name"
|
|
20
|
+
},
|
|
21
|
+
"fieldInput": {
|
|
22
|
+
"type": "autocomplete"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"operations": {
|
|
27
|
+
"findAll": {
|
|
28
|
+
"include": [
|
|
29
|
+
"author"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"findOne": {
|
|
33
|
+
"include": [
|
|
34
|
+
"author"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `relation.resource`: relative path to the target `resource.json`
|
|
42
|
+
- `relation.foreignKey`: FK column on **this** resource
|
|
43
|
+
- `relation.displayKey`: field on the related record shown in the dropdown
|
|
44
|
+
- `fieldInput.type`: `"autocomplete"` for manyToOne (renders a search dropdown)
|
|
45
|
+
|
|
46
|
+
## oneToMany (embedded table / panel)
|
|
47
|
+
|
|
48
|
+
Use when: another resource has a FK pointing to this one (e.g. `review.bookId → book.id`).
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"columns": {
|
|
53
|
+
"reviews": {
|
|
54
|
+
"label": "Reviews",
|
|
55
|
+
"hiddenInTable": true,
|
|
56
|
+
"hiddenInForm": true,
|
|
57
|
+
"relation": {
|
|
58
|
+
"resource": "./review/resource.json",
|
|
59
|
+
"type": "oneToMany",
|
|
60
|
+
"foreignKey": "bookId",
|
|
61
|
+
"sort": "createdAt",
|
|
62
|
+
"sortDir": "desc"
|
|
63
|
+
},
|
|
64
|
+
"fieldInput": {
|
|
65
|
+
"type": "relation"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"operations": {
|
|
70
|
+
"findOne": {
|
|
71
|
+
"include": [
|
|
72
|
+
"reviews"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `relation.foreignKey`: FK column on the **target** resource
|
|
80
|
+
- `relation.sort` / `relation.sortDir`: default sort for the related list
|
|
81
|
+
- `fieldInput.type`: `"relation"` renders an embedded table panel
|
|
82
|
+
- Include goes in `findOne.include` only (heavy — skip from `findAll`)
|
|
83
|
+
|
|
84
|
+
## manyToMany
|
|
85
|
+
|
|
86
|
+
Use when: a join table connects both resources.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"columns": {
|
|
91
|
+
"tags": {
|
|
92
|
+
"label": "Tags",
|
|
93
|
+
"relation": {
|
|
94
|
+
"resource": "./tag/resource.json",
|
|
95
|
+
"type": "manyToMany",
|
|
96
|
+
"foreignKey": "bookId",
|
|
97
|
+
"name": "tags"
|
|
98
|
+
},
|
|
99
|
+
"fieldInput": {
|
|
100
|
+
"type": "relation"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Detecting a relation column
|
|
108
|
+
|
|
109
|
+
Use `isRelation(col)` (exported from `@ghentcdh/crouton-core`) — returns `true` when `col.relation` is set.
|
|
110
|
+
Do not check `fieldInput.format` — that is removed.
|
|
111
|
+
|
|
112
|
+
## column.relation full reference
|
|
113
|
+
|
|
114
|
+
| Field | Description |
|
|
115
|
+
|--------------|------------------------------------------------------------|
|
|
116
|
+
| `resource` | Relative path to target `resource.json` |
|
|
117
|
+
| `type` | `"manyToOne"` \| `"oneToMany"` \| `"manyToMany"` |
|
|
118
|
+
| `foreignKey` | FK column name |
|
|
119
|
+
| `name` | Prisma relation field name (when different from column id) |
|
|
120
|
+
| `displayKey` | Field on the related record to show in autocomplete/lookup |
|
|
121
|
+
| `sort` | Default sort field for the related list |
|
|
122
|
+
| `sortDir` | `"asc"` \| `"desc"` (default `"asc"`) |
|
|
123
|
+
|
|
124
|
+
## include goes in operations, not at root
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
// WRONG
|
|
128
|
+
{
|
|
129
|
+
"include": [
|
|
130
|
+
"author"
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// RIGHT
|
|
135
|
+
{
|
|
136
|
+
"operations": {
|
|
137
|
+
"findAll": {
|
|
138
|
+
"include": [
|
|
139
|
+
"author"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
"findOne": {
|
|
143
|
+
"include": [
|
|
144
|
+
"author"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# resource.json — annotated full example
|
|
2
|
+
|
|
3
|
+
```json
|
|
4
|
+
{
|
|
5
|
+
"$schema": "node_modules/@ghentcdh/crouton-core/dist/resource.schema.json",
|
|
6
|
+
"schemaVersion": 1,
|
|
7
|
+
"name": "book",
|
|
8
|
+
"title": "Books",
|
|
9
|
+
"kind": "prisma",
|
|
10
|
+
"source": {
|
|
11
|
+
"model": "Book"
|
|
12
|
+
},
|
|
13
|
+
"lookup": {
|
|
14
|
+
"labels": ["title", "authorId"],
|
|
15
|
+
"separator": " · "
|
|
16
|
+
},
|
|
17
|
+
"openapi": {
|
|
18
|
+
"tag": "Books"
|
|
19
|
+
},
|
|
20
|
+
"columns": {
|
|
21
|
+
"id": {
|
|
22
|
+
"idField": true,
|
|
23
|
+
"hiddenInForm": true
|
|
24
|
+
},
|
|
25
|
+
"title": {
|
|
26
|
+
"label": "Title",
|
|
27
|
+
"fieldInput": { "type": "string" }
|
|
28
|
+
},
|
|
29
|
+
"pageCount": {
|
|
30
|
+
"label": "Page count",
|
|
31
|
+
"fieldInput": { "type": "number" }
|
|
32
|
+
},
|
|
33
|
+
"publishedAt": {
|
|
34
|
+
"label": "Published",
|
|
35
|
+
"fieldInput": { "type": "date" }
|
|
36
|
+
},
|
|
37
|
+
"genre": {
|
|
38
|
+
"label": "Genre",
|
|
39
|
+
"fieldInput": {
|
|
40
|
+
"type": "select",
|
|
41
|
+
"options": { "values": [
|
|
42
|
+
{ "value": "fiction", "label": "Fiction" },
|
|
43
|
+
{ "value": "non-fiction", "label": "Non-fiction" }
|
|
44
|
+
]}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"authorId": {
|
|
48
|
+
"label": "Author",
|
|
49
|
+
"relation": {
|
|
50
|
+
"resource": "./author/resource.json",
|
|
51
|
+
"type": "manyToOne",
|
|
52
|
+
"foreignKey": "authorId",
|
|
53
|
+
"displayKey": "name"
|
|
54
|
+
},
|
|
55
|
+
"fieldInput": { "type": "autocomplete" }
|
|
56
|
+
},
|
|
57
|
+
"chapterCount": {
|
|
58
|
+
"label": "Chapters",
|
|
59
|
+
"type": "number",
|
|
60
|
+
"computed": {
|
|
61
|
+
"sql": "SELECT COUNT(*) FROM chapter WHERE chapter.book_id = main.id"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"operations": {
|
|
66
|
+
"findAll": {
|
|
67
|
+
"search": ["title"],
|
|
68
|
+
"include": ["author"]
|
|
69
|
+
},
|
|
70
|
+
"findOne": {
|
|
71
|
+
"include": ["author", { "relation": "chapters", "orderBy": { "position": "asc" } }]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"layout": {
|
|
75
|
+
"form": {
|
|
76
|
+
"container": { "mode": "modal", "size": "lg" },
|
|
77
|
+
"type": "grid",
|
|
78
|
+
"columns": 12,
|
|
79
|
+
"items": [
|
|
80
|
+
{ "id": "title", "colspan": 9 },
|
|
81
|
+
{ "id": "genre", "colspan": 3 },
|
|
82
|
+
"pageCount",
|
|
83
|
+
"publishedAt",
|
|
84
|
+
"authorId",
|
|
85
|
+
{
|
|
86
|
+
"type": "collapse",
|
|
87
|
+
"title": "Metadata",
|
|
88
|
+
"items": ["createdAt", "updatedAt"]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"view": {
|
|
93
|
+
"container": { "mode": "page" }
|
|
94
|
+
},
|
|
95
|
+
"table": {
|
|
96
|
+
"pageSize": 50,
|
|
97
|
+
"pageSizes": [20, 50, 100],
|
|
98
|
+
"density": "compact",
|
|
99
|
+
"rowClick": "view",
|
|
100
|
+
"items": [
|
|
101
|
+
{ "id": "title", "sortable": true, "defaultSort": "asc", "width": "md", "truncate": 2 },
|
|
102
|
+
{ "id": "pageCount", "align": "right", "width": "xs" },
|
|
103
|
+
{ "id": "publishedAt", "sortable": true },
|
|
104
|
+
{ "id": "authorId" },
|
|
105
|
+
{ "id": "genre", "visible": false }
|
|
106
|
+
],
|
|
107
|
+
"searchFields": ["title"],
|
|
108
|
+
"filterFields": ["genre"],
|
|
109
|
+
"rowActions": [
|
|
110
|
+
{ "id": "preview", "type": "link", "href": "/preview/{id}", "label": "Preview" }
|
|
111
|
+
],
|
|
112
|
+
"tableActions": [
|
|
113
|
+
{ "id": "export", "label": "Export CSV", "procedure": "exportCsv" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Where does a key go?
|
|
121
|
+
|
|
122
|
+
| Concern | Key location |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Data shape / validation / form input | `columns[id].*` |
|
|
125
|
+
| How a record opens (modal vs page, size) | `layout.form.container` / `layout.view.container` |
|
|
126
|
+
| Table display (width, align, pin, truncate) | `layout.table.items[].options` or item fields |
|
|
127
|
+
| Storage (Prisma model, db table, datasource) | `source.*` |
|
|
128
|
+
| Query / endpoints (search, include, filter) | `operations.<op>.*` |
|
|
129
|
+
| Relation data model (type, FK, display field) | `columns[id].relation` |
|
|
130
|
+
| Dropdown label in other resources | `lookup.labels` |
|
|
131
|
+
| Navigation / sidebar | `sidebar` |
|
|
132
|
+
| Access control | `security` |
|
|
133
|
+
|
|
134
|
+
## Root-level key reference
|
|
135
|
+
|
|
136
|
+
| Field | Type | Required | Description |
|
|
137
|
+
|-------|------|----------|-------------|
|
|
138
|
+
| `$schema` | string | no | Path to JSON schema for editor autocomplete |
|
|
139
|
+
| `schemaVersion` | number | yes | Must be `1` |
|
|
140
|
+
| `name` | string | yes | Resource identifier (matches directory name) |
|
|
141
|
+
| `title` | string | yes | Display name for the resource |
|
|
142
|
+
| `kind` | `"prisma"` \| `"custom"` | no | `"prisma"` = Prisma-backed (default); `"custom"` = repository.ts |
|
|
143
|
+
| `source` | object | prisma only | Storage config — see below |
|
|
144
|
+
| `source.model` | string | prisma | Prisma model name (was root `model`) |
|
|
145
|
+
| `source.dbTable` | string | no | Override DB table name (was root `table`) |
|
|
146
|
+
| `source.datasource` | string | no | Named datasource from `crouton.json` (was root `database`) |
|
|
147
|
+
| `source.idType` | `"number"` \| `"string"` | no | PK type (was root `idType`) |
|
|
148
|
+
| `lookup` | object | no | How this resource labels itself in other resources' dropdowns |
|
|
149
|
+
| `lookup.labels` | string[] | no | Column ids to show (ordered); replaces per-column `showInLookup` |
|
|
150
|
+
| `lookup.separator` | string | no | Separator between label parts (default `" "`) |
|
|
151
|
+
| `openapi` | object | no | OpenAPI metadata |
|
|
152
|
+
| `openapi.tag` | string | no | API tag group name (default `"Crouton"`) |
|
|
153
|
+
| `columns` | object | yes | Map of column id → column config |
|
|
154
|
+
| `operations` | object | no | Per-operation query config |
|
|
155
|
+
| `layout` | object | no | Presentation config (form / view / table) |
|
|
156
|
+
| `sidebar` | object | no | Navigation placement |
|
|
157
|
+
| `security` | object | no | Role-based access rules |
|
|
158
|
+
|
|
159
|
+
## Column reference
|
|
160
|
+
|
|
161
|
+
| Field | Type | Description |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `label` | string | Human-readable label (defaults to title-cased id) |
|
|
164
|
+
| `idField` | boolean | Mark as primary-key column (exactly one per resource) |
|
|
165
|
+
| `hiddenInTable` | boolean | Hide in list view |
|
|
166
|
+
| `hiddenInForm` | boolean | Hide in create/edit form |
|
|
167
|
+
| `hiddenInView` | boolean | Hide in detail view |
|
|
168
|
+
| `createable` | boolean | Include in create form (default `true`) |
|
|
169
|
+
| `updateable` | boolean | Include in edit form (default `true`) |
|
|
170
|
+
| `required` | boolean | Override Prisma-derived required state in forms |
|
|
171
|
+
| `unique` | boolean \| UniqueConfig | Enforce uniqueness; object form allows `scope`, `caseInsensitive`, `message` |
|
|
172
|
+
| `type` | string \| object | Data type (`"string"`, `"number"`, `"boolean"`, `"date"`, `"date-time"`, `"object"`, `"array"`) or JSON Schema fragment |
|
|
173
|
+
| `fieldInput` | object | UI input widget config — see field-inputs.md |
|
|
174
|
+
| `relation` | object | Relation data model — see below |
|
|
175
|
+
| `computed` | object | SQL computed column — `{ sql, type? }` |
|
|
176
|
+
| `enum` | string | Shared enum name from `crouton.enums.json` (mutually exclusive with `fieldInput.options.values`) |
|
|
177
|
+
| `displayKey` | string | Dot-path to display a nested property |
|
|
178
|
+
| `extend` | string | Relative path to another resource.json to expand as sub-columns |
|
|
179
|
+
|
|
180
|
+
## column.relation reference
|
|
181
|
+
|
|
182
|
+
Put relation config here, not in `fieldInput.options`:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
"authorId": {
|
|
186
|
+
"relation": {
|
|
187
|
+
"resource": "./author/resource.json",
|
|
188
|
+
"type": "manyToOne",
|
|
189
|
+
"foreignKey": "authorId",
|
|
190
|
+
"name": "author",
|
|
191
|
+
"displayKey": "name",
|
|
192
|
+
"sort": "name",
|
|
193
|
+
"sortDir": "asc"
|
|
194
|
+
},
|
|
195
|
+
"fieldInput": { "type": "autocomplete" }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Field | Description |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `resource` | Relative path to target resource.json |
|
|
202
|
+
| `type` | `"manyToOne"` \| `"oneToMany"` \| `"manyToMany"` |
|
|
203
|
+
| `foreignKey` | FK column name (on this resource for manyToOne, on target for oneToMany) |
|
|
204
|
+
| `name` | Prisma relation field name (when different from column id) |
|
|
205
|
+
| `displayKey` | Field on the related record to show in the autocomplete |
|
|
206
|
+
| `sort` / `sortDir` | Default sort for the related list |
|
|
207
|
+
|
|
208
|
+
`isRelation(col)` returns `true` when `col.relation` is set.
|
|
209
|
+
|
|
210
|
+
## operations reference
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
"operations": {
|
|
214
|
+
"findAll": {
|
|
215
|
+
"search": ["title", "isbn"],
|
|
216
|
+
"include": ["author"]
|
|
217
|
+
},
|
|
218
|
+
"findOne": {
|
|
219
|
+
"include": ["author", { "relation": "chapters", "orderBy": { "position": "asc" } }]
|
|
220
|
+
},
|
|
221
|
+
"create": {},
|
|
222
|
+
"update": {},
|
|
223
|
+
"delete": {}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Root `include` is removed — set `findAll.include` and `findOne.include` separately so heavy relations can be loaded only on the detail view.
|
|
228
|
+
|
|
229
|
+
## layout.form / layout.view container
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
"layout": {
|
|
233
|
+
"form": {
|
|
234
|
+
"container": { "mode": "modal", "size": "lg" }
|
|
235
|
+
},
|
|
236
|
+
"view": {
|
|
237
|
+
"container": { "mode": "page" }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Field | Values | Description |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `container.mode` | `"modal"` \| `"page"` | How the record opens (default `"modal"`) |
|
|
245
|
+
| `container.size` | `"xs"` \| `"sm"` \| `"lg"` \| `"xl"` | Modal width (default `"sm"`) |
|
|
246
|
+
|
|
247
|
+
`display` and `modalSize` at root are removed — use `layout.<form\|view>.container`.
|
|
248
|
+
|
|
249
|
+
## layout.table item options
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
"items": [
|
|
253
|
+
{ "id": "title", "width": "md", "truncate": 2, "sortable": true, "defaultSort": "desc" },
|
|
254
|
+
{ "id": "pageCount", "align": "right", "width": "xs" },
|
|
255
|
+
{ "id": "status", "pin": "left" },
|
|
256
|
+
{ "id": "internalRef", "visible": false, "headerTooltip": "Internal reference number" }
|
|
257
|
+
]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Option | Type | Description |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `width` | `"xs"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` or CSS value | Column width |
|
|
263
|
+
| `minWidth` / `maxWidth` | CSS value | Width constraints |
|
|
264
|
+
| `align` | `"left"` \| `"center"` \| `"right"` | Cell text alignment |
|
|
265
|
+
| `truncate` | `boolean` \| number | `true` = single-line ellipsis; number = max lines before clamp |
|
|
266
|
+
| `pin` | `"left"` \| `"right"` | Sticky column |
|
|
267
|
+
| `visible` | boolean | `false` = hidden by default (no column chooser yet) |
|
|
268
|
+
| `headerTooltip` | string | Tooltip text on the column header |
|
|
269
|
+
| `defaultSort` | `true` \| `"asc"` \| `"desc"` | Initial sort; `true` = `"asc"` |
|
|
270
|
+
|
|
271
|
+
## layout.table node options
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
"table": {
|
|
275
|
+
"pageSize": 50,
|
|
276
|
+
"pageSizes": [20, 50, 100],
|
|
277
|
+
"density": "compact",
|
|
278
|
+
"selectable": true,
|
|
279
|
+
"rowClick": "view",
|
|
280
|
+
"emptyText": "No books yet",
|
|
281
|
+
"items": [...]
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
| Option | Description |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `pageSize` | Default page size (frontend hint; server still caps it) |
|
|
288
|
+
| `pageSizes` | Page size selector options |
|
|
289
|
+
| `density` | `"normal"` \| `"compact"` |
|
|
290
|
+
| `selectable` | Row checkboxes |
|
|
291
|
+
| `rowClick` | `"view"` \| `"edit"` \| `"none"` — what a row click opens |
|
|
292
|
+
| `emptyText` | Message shown when there are no rows |
|
|
293
|
+
|
|
294
|
+
## Layout items
|
|
295
|
+
|
|
296
|
+
Every entry in a node's `items` array is one of:
|
|
297
|
+
|
|
298
|
+
| Shape | Kind |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `"fieldName"` | Field id — renders field's default input |
|
|
301
|
+
| `{ "id": "fieldName", ... }` | Field control — field with overrides |
|
|
302
|
+
| `{ "component": "Name", ... }` | Custom Vue component |
|
|
303
|
+
| Object with `type` but no `id`/`component` | Nested container node |
|
|
304
|
+
|
|
305
|
+
## Table layout: flat vs wrapper
|
|
306
|
+
|
|
307
|
+
All table-specific settings (`rowActions`, `tableActions`, `searchFields`, `filterFields`, table options) go on the **`type: "table"` node**, never on an outer wrapper.
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
"table": {
|
|
311
|
+
"type": "grid",
|
|
312
|
+
"items": [
|
|
313
|
+
{ "component": "CreateByIsbn", "colspan": 12 },
|
|
314
|
+
{
|
|
315
|
+
"type": "table",
|
|
316
|
+
"pageSize": 50,
|
|
317
|
+
"items": ["title", "isbn"],
|
|
318
|
+
"searchFields": ["title", "isbn"]
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Layout is the full list
|
|
325
|
+
|
|
326
|
+
When `layout.<ctx>` is defined, its `items` list is exhaustive — only listed columns render. Columns not listed are hidden regardless of `hiddenIn*`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Crouton resource workflow
|
|
3
|
+
globs: ["**/resource.json", "**/datasource.json", "**/crouton.json"]
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Crouton skill
|
|
8
|
+
|
|
9
|
+
Crouton is a resource-driven admin UI framework. This skill teaches the correct workflow for adding and changing resources.
|
|
10
|
+
|
|
11
|
+
## What crouton is
|
|
12
|
+
- Resources live in `<resourcesDir>` (see `crouton.json`). Each is a folder with `resource.json`.
|
|
13
|
+
- Data sources live in `<dataSourcesDir>`. Each is a folder with `datasource.json`.
|
|
14
|
+
- The Prisma schema (`schema.prisma`) is the source of truth for models and columns. Never hand-edit column lists that `crouton update` owns.
|
|
15
|
+
|
|
16
|
+
## The workflow loop
|
|
17
|
+
|
|
18
|
+
1. Change `schema.prisma` (add/modify models or fields).
|
|
19
|
+
2. Run `crouton update` — regenerates column lists, migrations, and type stubs.
|
|
20
|
+
3. Edit `resource.json` (UI/behaviour: labels, field inputs, relations, layout).
|
|
21
|
+
4. Run `crouton validate` — fix any reported issues before proceeding.
|
|
22
|
+
5. Typecheck/build to confirm no TS errors.
|
|
23
|
+
|
|
24
|
+
## Key rules
|
|
25
|
+
- Never hand-edit auto-generated sections inside `resource.json` (marked with comments).
|
|
26
|
+
- Use `manyToOne` for autocomplete lookups; `oneToMany` for embedded tables.
|
|
27
|
+
- `kind: "custom"` only when you have a `repository.ts` (config-only resource). For non-Prisma data, use `adapter` in `datasource.json`.
|
|
28
|
+
- After any `resource.json` or `crouton.json` change, run `crouton validate`.
|
|
29
|
+
|
|
30
|
+
## References
|
|
31
|
+
- `references/resource-json.md` — annotated full example + key table
|
|
32
|
+
- `references/relations.md` — relation recipes
|
|
33
|
+
- `references/field-inputs.md` — which input for which column type
|
|
34
|
+
- `references/data-sources.md` — prisma vs custom adapter
|
|
35
|
+
- `references/recipes.md` — task recipes
|
|
36
|
+
- `references/common-mistakes.md` — common mistakes
|
|
37
|
+
|
|
38
|
+
## Docs for the installed version
|
|
39
|
+
`node_modules/@ghentcdh/crouton-core/agent-docs/README.md`
|