@omnifyjp/omnify 4.5.2 → 4.6.1

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.
@@ -0,0 +1,1195 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://omnify.dev/schemas/omnify-schema.json",
4
+ "title": "Omnify Schema Definition",
5
+ "description": "Schema definition for Omnify - a database-first schema generator for Laravel, TypeScript, and more.",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Schema name (usually derived from filename, but can be specified explicitly)"
11
+ },
12
+ "connection": {
13
+ "type": "string",
14
+ "description": "Database connection name (matches a key in omnify.yaml connections). Defaults to the 'default' connection if not specified."
15
+ },
16
+ "kind": {
17
+ "type": "string",
18
+ "enum": [
19
+ "object",
20
+ "enum",
21
+ "partial",
22
+ "pivot"
23
+ ],
24
+ "default": "object",
25
+ "description": "Schema kind - 'object' for database tables, 'enum' for enumeration types, 'partial' for extending existing schemas, 'pivot' for many-to-many join tables"
26
+ },
27
+ "target": {
28
+ "type": "string",
29
+ "description": "Target schema name for partial schemas (only used when kind is 'partial'). The partial schema's properties will be merged into the target."
30
+ },
31
+ "displayName": {
32
+ "$ref": "#/definitions/LocalizedString",
33
+ "description": "Human-readable display name (supports multi-language)"
34
+ },
35
+ "titleIndex": {
36
+ "type": "string",
37
+ "description": "Property to use as the title/label for records (e.g., 'name', 'title')"
38
+ },
39
+ "group": {
40
+ "type": "string",
41
+ "description": "Schema group for organization (e.g., 'auth', 'blog', 'shop')"
42
+ },
43
+ "options": {
44
+ "$ref": "#/definitions/SchemaOptions"
45
+ },
46
+ "properties": {
47
+ "type": "object",
48
+ "additionalProperties": {
49
+ "$ref": "#/definitions/PropertyDefinition"
50
+ },
51
+ "description": "Property definitions for this schema"
52
+ },
53
+ "values": {
54
+ "type": "array",
55
+ "items": {
56
+ "oneOf": [
57
+ {
58
+ "type": "string"
59
+ },
60
+ {
61
+ "$ref": "#/definitions/EnumValue"
62
+ }
63
+ ]
64
+ },
65
+ "description": "Enum values (only when kind is 'enum')"
66
+ },
67
+ "policies": {
68
+ "type": "array",
69
+ "items": {
70
+ "$ref": "#/definitions/PolicyDefinition"
71
+ },
72
+ "description": "Cedar-style ABAC policy definitions (only for kind: object schemas). Generates Laravel Policy classes."
73
+ }
74
+ },
75
+ "definitions": {
76
+ "LocalizedString": {
77
+ "oneOf": [
78
+ {
79
+ "type": "string"
80
+ },
81
+ {
82
+ "type": "object",
83
+ "additionalProperties": {
84
+ "type": "string"
85
+ },
86
+ "description": "Locale map (e.g., { ja: '日本語', en: 'English', vi: 'Tiếng Việt' })"
87
+ }
88
+ ],
89
+ "description": "A string or locale map for multi-language support"
90
+ },
91
+ "SchemaOptions": {
92
+ "type": "object",
93
+ "properties": {
94
+ "id": {
95
+ "oneOf": [
96
+ { "type": "boolean" },
97
+ { "type": "string", "enum": ["BigInt", "Int", "Uuid", "Ulid", "String"] }
98
+ ],
99
+ "default": true,
100
+ "description": "ID column: true (BigInt), false (no ID), or type string (BigInt, Int, Uuid, Ulid, String)"
101
+ },
102
+ "primaryKey": {
103
+ "oneOf": [
104
+ {
105
+ "type": "string",
106
+ "description": "Single column as primary key"
107
+ },
108
+ {
109
+ "type": "array",
110
+ "items": {
111
+ "type": "string"
112
+ },
113
+ "minItems": 1,
114
+ "description": "Composite primary key (multiple columns)"
115
+ }
116
+ ],
117
+ "description": "Custom primary key column(s). When set, automatically implies id: false (no auto-generated ID column)"
118
+ },
119
+ "timestamps": {
120
+ "type": "boolean",
121
+ "default": true,
122
+ "description": "Add created_at and updated_at timestamp columns"
123
+ },
124
+ "softDelete": {
125
+ "type": "boolean",
126
+ "default": false,
127
+ "description": "Add deleted_at column for soft deletes"
128
+ },
129
+ "tableName": {
130
+ "type": "string",
131
+ "description": "Custom table name (defaults to pluralized snake_case of schema name)"
132
+ },
133
+ "authenticatable": {
134
+ "type": "boolean",
135
+ "default": false,
136
+ "description": "Enable authenticatable trait (for User-like schemas)"
137
+ },
138
+ "indexes": {
139
+ "type": "array",
140
+ "items": {
141
+ "$ref": "#/definitions/IndexDefinition"
142
+ },
143
+ "description": "Database indexes for query optimization"
144
+ },
145
+ "hidden": {
146
+ "type": "boolean",
147
+ "default": false,
148
+ "description": "Hide from model generation (migrations still generated, but no Laravel/TypeScript models)"
149
+ },
150
+ "defaultOrder": {
151
+ "description": "Schema-level default ordering. Generates a global Eloquent scope on the base model so every query is sorted (bypass with ->withoutGlobalScope('defaultOrder')). Issue #40.",
152
+ "oneOf": [
153
+ { "type": "string" },
154
+ {
155
+ "type": "array",
156
+ "items": {
157
+ "type": "object",
158
+ "properties": {
159
+ "column": { "type": "string" },
160
+ "direction": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] }
161
+ },
162
+ "required": ["column"]
163
+ }
164
+ }
165
+ ]
166
+ },
167
+ "service": {
168
+ "type": "object",
169
+ "description": "Service layer codegen options — controls BaseService generation (searchable, filterable, eager loading, etc.)",
170
+ "additionalProperties": false,
171
+ "properties": {
172
+ "searchable": {
173
+ "type": "array",
174
+ "items": { "type": "string" },
175
+ "description": "Property names included in full-text search (?search=keyword). Overrides property-level searchable flags."
176
+ },
177
+ "filterable": {
178
+ "type": "array",
179
+ "items": { "type": "string" },
180
+ "description": "Property names available as exact-match filters. Overrides property-level filterable flags."
181
+ },
182
+ "defaultSort": {
183
+ "type": "string",
184
+ "description": "Default sort column. Prefix with '-' for descending (e.g., '-created_at')."
185
+ },
186
+ "eagerLoad": {
187
+ "type": "array",
188
+ "items": { "type": "string" },
189
+ "description": "Relations to eager-load (with()) on list/findById queries."
190
+ },
191
+ "eagerCount": {
192
+ "type": "array",
193
+ "items": { "type": "string" },
194
+ "description": "Relations to count (withCount()) on list/findById queries."
195
+ },
196
+ "lookupFields": {
197
+ "type": "array",
198
+ "items": { "type": "string" },
199
+ "description": "Fields returned by the lookup() method (lightweight select for dropdowns)."
200
+ }
201
+ }
202
+ },
203
+ "api": {
204
+ "type": "object",
205
+ "description": "CRUD API generation options",
206
+ "additionalProperties": false,
207
+ "properties": {
208
+ "prefix": {
209
+ "type": "string",
210
+ "description": "Route prefix (defaults to snake_plural schema name)"
211
+ },
212
+ "actions": {
213
+ "type": "array",
214
+ "items": { "type": "string", "enum": ["index", "store", "show", "update", "destroy"] },
215
+ "description": "CRUD actions to generate (defaults to all 5)"
216
+ },
217
+ "lookup": {
218
+ "type": "boolean",
219
+ "default": true,
220
+ "description": "Generate GET /lookup endpoint"
221
+ },
222
+ "bulkDelete": {
223
+ "type": "boolean",
224
+ "default": false,
225
+ "description": "Generate POST /bulk-delete endpoint"
226
+ },
227
+ "restore": {
228
+ "type": "boolean",
229
+ "description": "Generate POST /{id}/restore endpoint (requires softDelete)"
230
+ },
231
+ "middleware": {
232
+ "type": "array",
233
+ "items": { "type": "string" },
234
+ "description": "Additional middleware for API routes"
235
+ },
236
+ "perPage": {
237
+ "type": "integer",
238
+ "default": 15,
239
+ "description": "Default pagination size"
240
+ }
241
+ }
242
+ }
243
+ }
244
+ },
245
+ "IndexDefinition": {
246
+ "type": "object",
247
+ "required": [
248
+ "columns"
249
+ ],
250
+ "properties": {
251
+ "columns": {
252
+ "type": "array",
253
+ "items": {
254
+ "type": "string"
255
+ },
256
+ "description": "Columns to include in the index"
257
+ },
258
+ "unique": {
259
+ "type": "boolean",
260
+ "default": false,
261
+ "description": "Whether this is a unique index"
262
+ },
263
+ "name": {
264
+ "type": "string",
265
+ "description": "Custom index name"
266
+ },
267
+ "type": {
268
+ "type": "string",
269
+ "enum": [
270
+ "btree",
271
+ "hash",
272
+ "fulltext",
273
+ "spatial",
274
+ "gin",
275
+ "gist"
276
+ ],
277
+ "description": "Index type"
278
+ }
279
+ }
280
+ },
281
+ "PropertyDefinition": {
282
+ "oneOf": [
283
+ {
284
+ "$ref": "#/definitions/StringProperty"
285
+ },
286
+ {
287
+ "$ref": "#/definitions/NumericProperty"
288
+ },
289
+ {
290
+ "$ref": "#/definitions/BooleanProperty"
291
+ },
292
+ {
293
+ "$ref": "#/definitions/TextProperty"
294
+ },
295
+ {
296
+ "$ref": "#/definitions/DateTimeProperty"
297
+ },
298
+ {
299
+ "$ref": "#/definitions/TimestampProperty"
300
+ },
301
+ {
302
+ "$ref": "#/definitions/JsonProperty"
303
+ },
304
+ {
305
+ "$ref": "#/definitions/EnumProperty"
306
+ },
307
+ {
308
+ "$ref": "#/definitions/EnumRefProperty"
309
+ },
310
+ {
311
+ "$ref": "#/definitions/FileProperty"
312
+ },
313
+ {
314
+ "$ref": "#/definitions/AssociationProperty"
315
+ },
316
+ {
317
+ "$ref": "#/definitions/UuidProperty"
318
+ },
319
+ {
320
+ "$ref": "#/definitions/SpatialProperty"
321
+ }
322
+ ]
323
+ },
324
+ "ValidationRules": {
325
+ "type": "object",
326
+ "description": "Validation rules (application-level only, NOT database structure). Rules are additive — generators infer base rules from type, rules: adds/overrides on top. Each rule is only valid for specific types. Mutually exclusive: lowercase+uppercase, alpha/alphaNum/alphaDash/numeric (pick one), url+uuid, min+gt, max+lt, between+min/max/gt/lt, digits+digitsBetween.",
327
+ "properties": {
328
+ "required": {
329
+ "type": "boolean",
330
+ "default": false,
331
+ "description": "[All types] Override required/nullable inference. Does NOT affect database nullable."
332
+ },
333
+ "minLength": {
334
+ "type": "integer",
335
+ "minimum": 0,
336
+ "description": "[String types only] Minimum length. Must be <= maxLength when both set."
337
+ },
338
+ "maxLength": {
339
+ "type": "integer",
340
+ "minimum": 1,
341
+ "description": "[String types only] Maximum length. Overrides length-based max validation. If both maxLength and length are set and differ, omnify warns."
342
+ },
343
+ "url": {
344
+ "type": "boolean",
345
+ "description": "[String types only] Must be a valid URL. Mutually exclusive with uuid."
346
+ },
347
+ "uuid": {
348
+ "type": "boolean",
349
+ "description": "[String types only] Must be a valid UUID. Mutually exclusive with url."
350
+ },
351
+ "ip": {
352
+ "type": "boolean",
353
+ "description": "[String types only] Must be a valid IP address (v4 or v6). Makes ipv4/ipv6 redundant if set."
354
+ },
355
+ "ipv4": {
356
+ "type": "boolean",
357
+ "description": "[String types only] Must be a valid IPv4 address. Redundant when ip is set."
358
+ },
359
+ "ipv6": {
360
+ "type": "boolean",
361
+ "description": "[String types only] Must be a valid IPv6 address. Redundant when ip is set."
362
+ },
363
+ "alpha": {
364
+ "type": "boolean",
365
+ "description": "[String types only] Only alphabetic characters (a-z, A-Z). Mutually exclusive with alphaNum, alphaDash, numeric."
366
+ },
367
+ "alphaNum": {
368
+ "type": "boolean",
369
+ "description": "[String types only] Only alphanumeric characters (a-z, A-Z, 0-9). Mutually exclusive with alpha, alphaDash, numeric."
370
+ },
371
+ "alphaDash": {
372
+ "type": "boolean",
373
+ "description": "[String types only] Only alphanumeric, dash, underscore (a-z, A-Z, 0-9, -, _). Mutually exclusive with alpha, alphaNum, numeric."
374
+ },
375
+ "numeric": {
376
+ "type": "boolean",
377
+ "description": "[String types only] Only numeric characters (0-9). Mutually exclusive with alpha, alphaNum, alphaDash."
378
+ },
379
+ "digits": {
380
+ "type": "integer",
381
+ "minimum": 1,
382
+ "description": "[String types only] Must be exactly n digits. Mutually exclusive with digitsBetween."
383
+ },
384
+ "digitsBetween": {
385
+ "type": "array",
386
+ "items": {
387
+ "type": "integer",
388
+ "minimum": 1
389
+ },
390
+ "minItems": 2,
391
+ "maxItems": 2,
392
+ "description": "[String types only] Digit count between min and max. Format: [min, max], min <= max, both >= 1. Mutually exclusive with digits."
393
+ },
394
+ "startsWith": {
395
+ "oneOf": [
396
+ {
397
+ "type": "string"
398
+ },
399
+ {
400
+ "type": "array",
401
+ "items": {
402
+ "type": "string"
403
+ }
404
+ }
405
+ ],
406
+ "description": "[String types only] Must start with prefix(es)."
407
+ },
408
+ "endsWith": {
409
+ "oneOf": [
410
+ {
411
+ "type": "string"
412
+ },
413
+ {
414
+ "type": "array",
415
+ "items": {
416
+ "type": "string"
417
+ }
418
+ }
419
+ ],
420
+ "description": "[String types only] Must end with suffix(es)."
421
+ },
422
+ "lowercase": {
423
+ "type": "boolean",
424
+ "description": "[String types only] Must be entirely lowercase. Mutually exclusive with uppercase."
425
+ },
426
+ "uppercase": {
427
+ "type": "boolean",
428
+ "description": "[String types only] Must be entirely uppercase. Mutually exclusive with lowercase."
429
+ },
430
+ "min": {
431
+ "type": "number",
432
+ "description": "[Numeric types only] Minimum value (inclusive). Must be <= max when both set. Mutually exclusive with gt. Redundant when between is set."
433
+ },
434
+ "max": {
435
+ "type": "number",
436
+ "description": "[Numeric types only] Maximum value (inclusive). Must be >= min when both set. Mutually exclusive with lt. Redundant when between is set."
437
+ },
438
+ "between": {
439
+ "type": "array",
440
+ "items": {
441
+ "type": "number"
442
+ },
443
+ "minItems": 2,
444
+ "maxItems": 2,
445
+ "description": "[Numeric types only] Value between min and max (inclusive). Format: [min, max], min <= max. Do not combine with min/max/gt/lt."
446
+ },
447
+ "gt": {
448
+ "type": "number",
449
+ "description": "[Numeric types only] Must be greater than value (exclusive). Must be < lt when both set. Mutually exclusive with min. Redundant when between is set."
450
+ },
451
+ "lt": {
452
+ "type": "number",
453
+ "description": "[Numeric types only] Must be less than value (exclusive). Must be > gt when both set. Mutually exclusive with max. Redundant when between is set."
454
+ },
455
+ "multipleOf": {
456
+ "type": "number",
457
+ "exclusiveMinimum": 0,
458
+ "description": "[Numeric types only] Must be a multiple of value. Must be > 0."
459
+ },
460
+ "arrayMin": {
461
+ "type": "integer",
462
+ "minimum": 0,
463
+ "description": "[Array/Json types only] Minimum number of items. Must be <= arrayMax when both set."
464
+ },
465
+ "arrayMax": {
466
+ "type": "integer",
467
+ "minimum": 1,
468
+ "description": "[Array/Json types only] Maximum number of items. Must be >= arrayMin when both set."
469
+ }
470
+ }
471
+ },
472
+ "BaseProperty": {
473
+ "type": "object",
474
+ "properties": {
475
+ "displayName": {
476
+ "$ref": "#/definitions/LocalizedString",
477
+ "description": "Human-readable display name (supports multi-language)"
478
+ },
479
+ "description": {
480
+ "$ref": "#/definitions/LocalizedString",
481
+ "description": "Field description/comment"
482
+ },
483
+ "placeholder": {
484
+ "$ref": "#/definitions/LocalizedString",
485
+ "description": "Placeholder text for form inputs (supports multi-language)"
486
+ },
487
+ "nullable": {
488
+ "type": "boolean",
489
+ "default": false,
490
+ "description": "Whether the field can be null in database"
491
+ },
492
+ "unique": {
493
+ "type": "boolean",
494
+ "default": false,
495
+ "description": "Whether this field must be unique"
496
+ },
497
+ "primary": {
498
+ "type": "boolean",
499
+ "default": false,
500
+ "description": "Whether this field is the primary key. Use with options.id: false for tables with custom primary keys (e.g., cache tables)"
501
+ },
502
+ "default": {
503
+ "description": "Default value for the field"
504
+ },
505
+ "rules": {
506
+ "$ref": "#/definitions/ValidationRules",
507
+ "description": "Validation rules (application-level only, NOT database)"
508
+ },
509
+ "hidden": {
510
+ "type": "boolean",
511
+ "default": false,
512
+ "description": "Whether this field should be hidden in API responses/serialization"
513
+ },
514
+ "fillable": {
515
+ "type": "boolean",
516
+ "default": true,
517
+ "description": "Whether this field is mass assignable (Laravel)"
518
+ },
519
+ "searchable": {
520
+ "type": "boolean",
521
+ "description": "Include in full-text search (?search=keyword)"
522
+ },
523
+ "filterable": {
524
+ "type": "boolean",
525
+ "description": "Allow filtering (?field=value or ?field_min=N&field_max=N)"
526
+ },
527
+ "sortable": {
528
+ "type": "boolean",
529
+ "description": "Allow sorting (?sort=field or ?sort=-field)"
530
+ },
531
+ "fields": {
532
+ "$ref": "#/definitions/CompoundFieldOverrides",
533
+ "description": "Per-field settings for compound types (nullable, hidden, fillable, placeholder overrides)"
534
+ }
535
+ }
536
+ },
537
+ "CompoundFieldOverrides": {
538
+ "type": "object",
539
+ "additionalProperties": {
540
+ "$ref": "#/definitions/CompoundFieldOverride"
541
+ },
542
+ "description": "Per-field overrides for compound types. Keys are field suffixes (e.g., 'Lastname', 'PostalCode')"
543
+ },
544
+ "CompoundFieldOverride": {
545
+ "type": "object",
546
+ "properties": {
547
+ "nullable": {
548
+ "type": "boolean",
549
+ "description": "Override nullable for this field"
550
+ },
551
+ "hidden": {
552
+ "type": "boolean",
553
+ "description": "Override hidden for this field"
554
+ },
555
+ "fillable": {
556
+ "type": "boolean",
557
+ "description": "Override fillable for this field"
558
+ },
559
+ "length": {
560
+ "type": "integer",
561
+ "minimum": 1,
562
+ "description": "Override length for string fields"
563
+ },
564
+ "displayName": {
565
+ "$ref": "#/definitions/LocalizedString",
566
+ "description": "Override display name (label) for this field (supports multi-language)"
567
+ },
568
+ "placeholder": {
569
+ "$ref": "#/definitions/LocalizedString",
570
+ "description": "Override placeholder for this field (supports multi-language)"
571
+ }
572
+ }
573
+ },
574
+ "StringProperty": {
575
+ "allOf": [
576
+ {
577
+ "$ref": "#/definitions/BaseProperty"
578
+ },
579
+ {
580
+ "type": "object",
581
+ "required": [
582
+ "type"
583
+ ],
584
+ "properties": {
585
+ "type": {
586
+ "type": "string",
587
+ "enum": [
588
+ "String",
589
+ "Email",
590
+ "Password"
591
+ ],
592
+ "description": "String type"
593
+ },
594
+ "length": {
595
+ "type": "integer",
596
+ "minimum": 1,
597
+ "maximum": 65535,
598
+ "default": 255,
599
+ "description": "Database column size (NOT validation). Use rules.maxLength for validation."
600
+ }
601
+ }
602
+ }
603
+ ]
604
+ },
605
+ "NumericProperty": {
606
+ "allOf": [
607
+ {
608
+ "$ref": "#/definitions/BaseProperty"
609
+ },
610
+ {
611
+ "type": "object",
612
+ "required": [
613
+ "type"
614
+ ],
615
+ "properties": {
616
+ "type": {
617
+ "type": "string",
618
+ "enum": [
619
+ "TinyInt",
620
+ "Int",
621
+ "BigInt",
622
+ "Float",
623
+ "Decimal"
624
+ ],
625
+ "description": "Numeric type - TinyInt (8-bit), Int (32-bit), BigInt (64-bit)"
626
+ },
627
+ "unsigned": {
628
+ "type": "boolean",
629
+ "default": false,
630
+ "description": "Whether the number is unsigned (positive only)"
631
+ },
632
+ "precision": {
633
+ "type": "integer",
634
+ "description": "Total number of digits for Decimal (default: 8)"
635
+ },
636
+ "scale": {
637
+ "type": "integer",
638
+ "description": "Number of decimal places for Decimal (default: 2)"
639
+ }
640
+ }
641
+ }
642
+ ]
643
+ },
644
+ "BooleanProperty": {
645
+ "allOf": [
646
+ {
647
+ "$ref": "#/definitions/BaseProperty"
648
+ },
649
+ {
650
+ "type": "object",
651
+ "required": [
652
+ "type"
653
+ ],
654
+ "properties": {
655
+ "type": {
656
+ "const": "Boolean",
657
+ "description": "Boolean type"
658
+ }
659
+ }
660
+ }
661
+ ]
662
+ },
663
+ "TextProperty": {
664
+ "allOf": [
665
+ {
666
+ "$ref": "#/definitions/BaseProperty"
667
+ },
668
+ {
669
+ "type": "object",
670
+ "required": [
671
+ "type"
672
+ ],
673
+ "properties": {
674
+ "type": {
675
+ "type": "string",
676
+ "enum": [
677
+ "Text",
678
+ "MediumText",
679
+ "LongText"
680
+ ],
681
+ "description": "Text type - Text (~65KB), MediumText (~16MB), LongText (~4GB)"
682
+ }
683
+ }
684
+ }
685
+ ]
686
+ },
687
+ "DateTimeProperty": {
688
+ "allOf": [
689
+ {
690
+ "$ref": "#/definitions/BaseProperty"
691
+ },
692
+ {
693
+ "type": "object",
694
+ "required": [
695
+ "type"
696
+ ],
697
+ "properties": {
698
+ "type": {
699
+ "type": "string",
700
+ "enum": [
701
+ "Date",
702
+ "DateTime",
703
+ "Time"
704
+ ],
705
+ "description": "Date/Time type"
706
+ }
707
+ }
708
+ }
709
+ ]
710
+ },
711
+ "TimestampProperty": {
712
+ "allOf": [
713
+ {
714
+ "$ref": "#/definitions/BaseProperty"
715
+ },
716
+ {
717
+ "type": "object",
718
+ "required": [
719
+ "type"
720
+ ],
721
+ "properties": {
722
+ "type": {
723
+ "const": "Timestamp",
724
+ "description": "Timestamp type"
725
+ },
726
+ "useCurrent": {
727
+ "type": "boolean",
728
+ "default": false,
729
+ "description": "Use CURRENT_TIMESTAMP as default value (Laravel: useCurrent())"
730
+ },
731
+ "useCurrentOnUpdate": {
732
+ "type": "boolean",
733
+ "default": false,
734
+ "description": "Update to CURRENT_TIMESTAMP on row update (Laravel: useCurrentOnUpdate())"
735
+ }
736
+ }
737
+ }
738
+ ]
739
+ },
740
+ "JsonProperty": {
741
+ "allOf": [
742
+ {
743
+ "$ref": "#/definitions/BaseProperty"
744
+ },
745
+ {
746
+ "type": "object",
747
+ "required": [
748
+ "type"
749
+ ],
750
+ "properties": {
751
+ "type": {
752
+ "const": "Json",
753
+ "description": "JSON type for storing structured data"
754
+ }
755
+ }
756
+ }
757
+ ]
758
+ },
759
+ "EnumProperty": {
760
+ "allOf": [
761
+ {
762
+ "$ref": "#/definitions/BaseProperty"
763
+ },
764
+ {
765
+ "type": "object",
766
+ "required": [
767
+ "type",
768
+ "enum"
769
+ ],
770
+ "properties": {
771
+ "type": {
772
+ "const": "Enum",
773
+ "description": "Inline enum type"
774
+ },
775
+ "enum": {
776
+ "type": "array",
777
+ "items": {
778
+ "oneOf": [
779
+ {
780
+ "type": "string"
781
+ },
782
+ {
783
+ "$ref": "#/definitions/EnumValue"
784
+ }
785
+ ]
786
+ },
787
+ "description": "Inline enum values"
788
+ }
789
+ }
790
+ }
791
+ ]
792
+ },
793
+ "EnumRefProperty": {
794
+ "allOf": [
795
+ {
796
+ "$ref": "#/definitions/BaseProperty"
797
+ },
798
+ {
799
+ "type": "object",
800
+ "required": [
801
+ "type",
802
+ "enum"
803
+ ],
804
+ "properties": {
805
+ "type": {
806
+ "const": "EnumRef",
807
+ "description": "Reference to a shared enum schema"
808
+ },
809
+ "enum": {
810
+ "type": "string",
811
+ "description": "Name of the enum schema to reference"
812
+ }
813
+ }
814
+ }
815
+ ]
816
+ },
817
+ "EnumValue": {
818
+ "type": "object",
819
+ "required": [
820
+ "value"
821
+ ],
822
+ "properties": {
823
+ "value": {
824
+ "type": "string",
825
+ "description": "Enum value (stored in database)"
826
+ },
827
+ "label": {
828
+ "$ref": "#/definitions/LocalizedString",
829
+ "description": "Human-readable label (supports multi-language)"
830
+ },
831
+ "extra": {
832
+ "type": "object",
833
+ "description": "Additional metadata for the enum value"
834
+ }
835
+ }
836
+ },
837
+ "FileProperty": {
838
+ "allOf": [
839
+ {
840
+ "$ref": "#/definitions/BaseProperty"
841
+ },
842
+ {
843
+ "type": "object",
844
+ "required": [
845
+ "type"
846
+ ],
847
+ "properties": {
848
+ "type": {
849
+ "const": "File",
850
+ "description": "File attachment (polymorphic relation)"
851
+ },
852
+ "multiple": {
853
+ "type": "boolean",
854
+ "default": false,
855
+ "description": "Allow multiple files"
856
+ },
857
+ "maxFiles": {
858
+ "type": "integer",
859
+ "description": "Maximum number of files (only when multiple=true)"
860
+ },
861
+ "accept": {
862
+ "type": "array",
863
+ "items": {
864
+ "type": "string"
865
+ },
866
+ "description": "Accepted file extensions (e.g., ['jpg', 'png', 'pdf'])"
867
+ },
868
+ "maxSize": {
869
+ "type": "integer",
870
+ "description": "Maximum file size in KB"
871
+ },
872
+ "collection": {
873
+ "type": "string",
874
+ "description": "Logical file collection name (defaults to property name)"
875
+ }
876
+ }
877
+ }
878
+ ]
879
+ },
880
+ "UuidProperty": {
881
+ "allOf": [
882
+ {
883
+ "$ref": "#/definitions/BaseProperty"
884
+ },
885
+ {
886
+ "type": "object",
887
+ "required": [
888
+ "type"
889
+ ],
890
+ "properties": {
891
+ "type": {
892
+ "const": "Uuid",
893
+ "description": "UUID type - stored as CHAR(36)"
894
+ }
895
+ }
896
+ }
897
+ ]
898
+ },
899
+ "SpatialProperty": {
900
+ "allOf": [
901
+ {
902
+ "$ref": "#/definitions/BaseProperty"
903
+ },
904
+ {
905
+ "type": "object",
906
+ "required": [
907
+ "type"
908
+ ],
909
+ "properties": {
910
+ "type": {
911
+ "type": "string",
912
+ "enum": [
913
+ "Point",
914
+ "Coordinates"
915
+ ],
916
+ "description": "Spatial type - Point (single lat/lng), Coordinates (lat/lng pair columns)"
917
+ }
918
+ }
919
+ }
920
+ ]
921
+ },
922
+ "PolicyDefinition": {
923
+ "type": "object",
924
+ "required": ["effect", "actions"],
925
+ "properties": {
926
+ "effect": {
927
+ "type": "string",
928
+ "enum": ["permit", "forbid"],
929
+ "description": "Policy effect — permit allows access, forbid denies access"
930
+ },
931
+ "actions": {
932
+ "oneOf": [
933
+ {
934
+ "type": "array",
935
+ "items": {
936
+ "type": "string",
937
+ "enum": ["view", "list", "create", "edit", "delete", "*"]
938
+ }
939
+ },
940
+ {
941
+ "type": "string",
942
+ "enum": ["view", "list", "create", "edit", "delete", "*"]
943
+ }
944
+ ],
945
+ "description": "Actions this policy applies to. Use '*' for all actions."
946
+ },
947
+ "when": {
948
+ "oneOf": [
949
+ { "type": "string" },
950
+ {
951
+ "type": "array",
952
+ "items": { "type": "string" }
953
+ }
954
+ ],
955
+ "description": "Condition(s) for this policy. String for single condition, array for AND conditions."
956
+ },
957
+ "desc": {
958
+ "type": "string",
959
+ "description": "Human-readable description (becomes PHP comment in generated code)"
960
+ }
961
+ }
962
+ },
963
+ "AssociationProperty": {
964
+ "type": "object",
965
+ "required": [
966
+ "type",
967
+ "relation"
968
+ ],
969
+ "properties": {
970
+ "type": {
971
+ "const": "Association",
972
+ "description": "Relationship to another schema"
973
+ },
974
+ "relation": {
975
+ "type": "string",
976
+ "enum": [
977
+ "OneToOne",
978
+ "OneToMany",
979
+ "ManyToOne",
980
+ "ManyToMany",
981
+ "MorphTo",
982
+ "MorphOne",
983
+ "MorphMany",
984
+ "MorphToMany",
985
+ "MorphedByMany"
986
+ ],
987
+ "description": "Relationship cardinality type"
988
+ },
989
+ "target": {
990
+ "type": "string",
991
+ "description": "Target schema name (e.g., 'User', 'Post')"
992
+ },
993
+ "displayName": {
994
+ "$ref": "#/definitions/LocalizedString",
995
+ "description": "Human-readable display name"
996
+ },
997
+ "onDelete": {
998
+ "type": "string",
999
+ "enum": [
1000
+ "CASCADE",
1001
+ "SET NULL",
1002
+ "SET DEFAULT",
1003
+ "RESTRICT",
1004
+ "NO ACTION"
1005
+ ],
1006
+ "description": "Action when referenced record is deleted"
1007
+ },
1008
+ "onUpdate": {
1009
+ "type": "string",
1010
+ "enum": [
1011
+ "CASCADE",
1012
+ "SET NULL",
1013
+ "SET DEFAULT",
1014
+ "RESTRICT",
1015
+ "NO ACTION"
1016
+ ],
1017
+ "description": "Action when referenced record is updated"
1018
+ },
1019
+ "joinTable": {
1020
+ "type": "string",
1021
+ "description": "Custom join table name for ManyToMany"
1022
+ },
1023
+ "inversedBy": {
1024
+ "type": "string",
1025
+ "description": "Property name on target that maps back"
1026
+ },
1027
+ "mappedBy": {
1028
+ "type": "string",
1029
+ "description": "Property name that is mapped by target"
1030
+ },
1031
+ "orderBy": {
1032
+ "description": "Default ordering for OneToMany/ManyToMany/MorphMany. Single column shorthand or list of {column, direction}. Issue #40.",
1033
+ "oneOf": [
1034
+ { "type": "string" },
1035
+ {
1036
+ "type": "array",
1037
+ "items": {
1038
+ "type": "object",
1039
+ "properties": {
1040
+ "column": { "type": "string" },
1041
+ "direction": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] }
1042
+ },
1043
+ "required": ["column"]
1044
+ }
1045
+ }
1046
+ ]
1047
+ },
1048
+ "targets": {
1049
+ "type": "array",
1050
+ "items": {
1051
+ "type": "string"
1052
+ },
1053
+ "description": "Target schema names for MorphTo (e.g., ['Post', 'Video'])"
1054
+ },
1055
+ "morphName": {
1056
+ "type": "string",
1057
+ "description": "Custom morph name for polymorphic relations"
1058
+ },
1059
+ "nullable": {
1060
+ "type": "boolean",
1061
+ "default": true,
1062
+ "description": "Whether the relation columns are nullable (default: true for MorphTo)"
1063
+ },
1064
+ "targetNamespace": {
1065
+ "type": "string",
1066
+ "description": "Target model namespace for external packages (e.g., 'Omnify\\SsoClient\\Models')"
1067
+ },
1068
+ "idType": {
1069
+ "type": "string",
1070
+ "enum": ["Int", "BigInt", "Uuid", "Ulid", "String"],
1071
+ "description": "Target primary key type for FK column generation"
1072
+ }
1073
+ }
1074
+ }
1075
+ },
1076
+ "examples": [
1077
+ {
1078
+ "name": "User",
1079
+ "displayName": {
1080
+ "ja": "ユーザー",
1081
+ "en": "User"
1082
+ },
1083
+ "options": {
1084
+ "softDelete": true,
1085
+ "authenticatable": true
1086
+ },
1087
+ "properties": {
1088
+ "name": {
1089
+ "type": "String",
1090
+ "displayName": {
1091
+ "ja": "氏名",
1092
+ "en": "Full Name"
1093
+ }
1094
+ },
1095
+ "email": {
1096
+ "type": "Email",
1097
+ "unique": true,
1098
+ "displayName": {
1099
+ "ja": "メールアドレス",
1100
+ "en": "Email"
1101
+ }
1102
+ },
1103
+ "password": {
1104
+ "type": "Password"
1105
+ },
1106
+ "posts": {
1107
+ "type": "Association",
1108
+ "relation": "OneToMany",
1109
+ "target": "Post"
1110
+ }
1111
+ }
1112
+ },
1113
+ {
1114
+ "name": "Post",
1115
+ "displayName": {
1116
+ "ja": "投稿",
1117
+ "en": "Post"
1118
+ },
1119
+ "options": {
1120
+ "softDelete": true,
1121
+ "indexes": [
1122
+ {
1123
+ "columns": [
1124
+ "published_at"
1125
+ ]
1126
+ },
1127
+ {
1128
+ "columns": [
1129
+ "status",
1130
+ "published_at"
1131
+ ]
1132
+ }
1133
+ ]
1134
+ },
1135
+ "properties": {
1136
+ "title": {
1137
+ "type": "String",
1138
+ "displayName": {
1139
+ "ja": "タイトル",
1140
+ "en": "Title"
1141
+ }
1142
+ },
1143
+ "content": {
1144
+ "type": "LongText",
1145
+ "displayName": {
1146
+ "ja": "本文",
1147
+ "en": "Content"
1148
+ }
1149
+ },
1150
+ "status": {
1151
+ "type": "EnumRef",
1152
+ "enum": "PostStatus",
1153
+ "default": "draft"
1154
+ },
1155
+ "author": {
1156
+ "type": "Association",
1157
+ "relation": "ManyToOne",
1158
+ "target": "User",
1159
+ "onDelete": "CASCADE"
1160
+ }
1161
+ }
1162
+ },
1163
+ {
1164
+ "name": "PostStatus",
1165
+ "kind": "enum",
1166
+ "displayName": {
1167
+ "ja": "投稿ステータス",
1168
+ "en": "Post Status"
1169
+ },
1170
+ "values": [
1171
+ {
1172
+ "value": "draft",
1173
+ "label": {
1174
+ "ja": "下書き",
1175
+ "en": "Draft"
1176
+ }
1177
+ },
1178
+ {
1179
+ "value": "published",
1180
+ "label": {
1181
+ "ja": "公開済み",
1182
+ "en": "Published"
1183
+ }
1184
+ },
1185
+ {
1186
+ "value": "archived",
1187
+ "label": {
1188
+ "ja": "アーカイブ",
1189
+ "en": "Archived"
1190
+ }
1191
+ }
1192
+ ]
1193
+ }
1194
+ ]
1195
+ }