@omnifyjp/ts 1.1.3 → 1.1.5

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/dist/cli.js CHANGED
@@ -52,6 +52,7 @@ function resolveFromConfig(configPath) {
52
52
  resource: laravelConfig?.resource,
53
53
  factory: laravelConfig?.factory,
54
54
  provider: laravelConfig?.provider,
55
+ policy: laravelConfig?.policy,
55
56
  } : undefined;
56
57
  return {
57
58
  input: resolve(configDir, schemasPath),
package/dist/php/index.js CHANGED
@@ -21,6 +21,7 @@ import { generateResources } from './resource-generator.js';
21
21
  import { generateFactories } from './factory-generator.js';
22
22
  import { generateServiceProvider } from './service-provider-generator.js';
23
23
  import { generateTranslationModels } from './translation-model-generator.js';
24
+ import { generatePolicies } from './policy-generator.js';
24
25
  export { derivePhpConfig } from './types.js';
25
26
  /** Generate all PHP files from schemas.json data. */
26
27
  export function generatePhp(data, overrides) {
@@ -38,5 +39,6 @@ export function generatePhp(data, overrides) {
38
39
  files.push(...generateRequests(reader, config));
39
40
  files.push(...generateResources(reader, config));
40
41
  files.push(...generateFactories(reader, config));
42
+ files.push(...generatePolicies(reader, config));
41
43
  return files;
42
44
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Laravel Policy class generator.
3
+ *
4
+ * Generates Policy classes from Cedar-style ABAC policy definitions.
5
+ * Only generates for schemas that have `policies` defined.
6
+ */
7
+ import { SchemaReader } from './schema-reader.js';
8
+ import type { GeneratedFile, PhpConfig } from './types.js';
9
+ import type { SchemaDefinition, SingleCondition } from '../types.js';
10
+ /** Generate Laravel Policy classes for all schemas that have policies. */
11
+ export declare function generatePolicies(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
12
+ /** Convert a single condition to a PHP expression. */
13
+ export declare function conditionToPhp(cond: SingleCondition, schema: SchemaDefinition, reader: SchemaReader): string;
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Laravel Policy class generator.
3
+ *
4
+ * Generates Policy classes from Cedar-style ABAC policy definitions.
5
+ * Only generates for schemas that have `policies` defined.
6
+ */
7
+ import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
8
+ import { baseFile, userFile } from './types.js';
9
+ // ============================================================================
10
+ // Action mapping: Omnify → Laravel
11
+ // ============================================================================
12
+ /** Maps omnify action names to Laravel policy method names. */
13
+ const ACTION_METHOD_MAP = {
14
+ view: 'view',
15
+ list: 'viewAny',
16
+ create: 'create',
17
+ edit: 'update',
18
+ delete: 'delete',
19
+ };
20
+ /** All standard actions (used for wildcard expansion). */
21
+ const ALL_ACTIONS = ['view', 'list', 'create', 'edit', 'delete'];
22
+ /** Actions that receive both $user and $record parameters. */
23
+ const RECORD_ACTIONS = new Set(['view', 'edit', 'delete']);
24
+ // ============================================================================
25
+ // Public API
26
+ // ============================================================================
27
+ /** Generate Laravel Policy classes for all schemas that have policies. */
28
+ export function generatePolicies(reader, config) {
29
+ const files = [];
30
+ for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
31
+ if (!schema.policies || schema.policies.length === 0)
32
+ continue;
33
+ files.push(...generateForSchema(name, schema, reader, config));
34
+ }
35
+ return files;
36
+ }
37
+ // ============================================================================
38
+ // Per-schema generation
39
+ // ============================================================================
40
+ function generateForSchema(name, schema, reader, config) {
41
+ return [
42
+ generateBasePolicyClass(name, schema, reader, config),
43
+ generateUserPolicyClass(name, config),
44
+ ];
45
+ }
46
+ function generateBasePolicyClass(name, schema, reader, config) {
47
+ const modelName = toPascalCase(name);
48
+ const baseNamespace = config.policies.baseNamespace;
49
+ const modelNamespace = config.models.namespace;
50
+ const policies = schema.policies;
51
+ const hasSoftDelete = schema.options?.softDelete ?? false;
52
+ const needsCidrHelper = policiesUseCidr(policies);
53
+ // Collect all actions that have policies
54
+ const expandedPolicies = expandWildcards(policies);
55
+ // Build methods
56
+ const methods = [];
57
+ for (const action of ALL_ACTIONS) {
58
+ const methodName = ACTION_METHOD_MAP[action];
59
+ const hasRecord = RECORD_ACTIONS.has(action);
60
+ const body = generateMethodBody(action, expandedPolicies, schema, reader, hasRecord);
61
+ methods.push(buildMethod(methodName, modelName, hasRecord, body));
62
+ }
63
+ // SoftDelete: restore + forceDelete
64
+ if (hasSoftDelete) {
65
+ const restoreBody = generateMethodBody('delete', expandedPolicies, schema, reader);
66
+ methods.push(buildMethod('restore', modelName, true, restoreBody));
67
+ const forceDeleteBody = generateMethodBody('delete', expandedPolicies, schema, reader);
68
+ methods.push(buildMethod('forceDelete', modelName, true, forceDeleteBody));
69
+ }
70
+ const cidrHelper = needsCidrHelper ? buildCidrHelper() : '';
71
+ const content = `<?php
72
+
73
+ namespace ${baseNamespace};
74
+
75
+ /**
76
+ * DO NOT EDIT - This file is auto-generated by Omnify.
77
+ * Any changes will be overwritten on next generation.
78
+ *
79
+ * @generated by omnify
80
+ */
81
+
82
+ use ${modelNamespace}\\${modelName};
83
+ use App\\Models\\User;
84
+ use Illuminate\\Auth\\Access\\HandlesAuthorization;
85
+
86
+ class ${modelName}PolicyBase
87
+ {
88
+ use HandlesAuthorization;
89
+ ${methods.join('\n')}${cidrHelper}
90
+ }
91
+ `;
92
+ return baseFile(`${config.policies.basePath}/${modelName}PolicyBase.php`, content);
93
+ }
94
+ function generateUserPolicyClass(name, config) {
95
+ const modelName = toPascalCase(name);
96
+ const policyNamespace = config.policies.namespace;
97
+ const baseNamespace = config.policies.baseNamespace;
98
+ const content = `<?php
99
+
100
+ namespace ${policyNamespace};
101
+
102
+ use ${baseNamespace}\\${modelName}PolicyBase;
103
+
104
+ /**
105
+ * ${modelName} Policy
106
+ *
107
+ * This file is generated once and can be customized.
108
+ * Add your custom authorization logic here.
109
+ */
110
+ class ${modelName}Policy extends ${modelName}PolicyBase
111
+ {
112
+ // Add your custom policy methods here
113
+ }
114
+ `;
115
+ return userFile(`${config.policies.path}/${modelName}Policy.php`, content);
116
+ }
117
+ // ============================================================================
118
+ // Method body generation
119
+ // ============================================================================
120
+ /**
121
+ * Generate the body of a policy method.
122
+ * Cedar evaluation order: forbid first → permit → default deny.
123
+ *
124
+ * When hasRecord is false (e.g. viewAny, create), conditions referencing
125
+ * $record properties are dropped. A conditional policy whose condition is
126
+ * entirely record-dependent becomes unconditional.
127
+ */
128
+ function generateMethodBody(action, policies, schema, reader, hasRecord = true) {
129
+ const relevantPolicies = policies.filter(p => p.actions.includes(action));
130
+ if (relevantPolicies.length === 0) {
131
+ return ' return false;';
132
+ }
133
+ const lines = [];
134
+ // 1. Forbid rules first
135
+ const forbids = relevantPolicies.filter(p => p.effect === 'forbid');
136
+ for (const policy of forbids) {
137
+ if (policy.when) {
138
+ if (!hasRecord && whenUsesRecord(policy.when)) {
139
+ // Skip record-dependent conditions for collection methods
140
+ continue;
141
+ }
142
+ const condition = whenToPhp(policy.when, schema, reader);
143
+ lines.push(` // ${policy.desc ?? 'Forbid rule'}`);
144
+ lines.push(` if (${condition}) {`);
145
+ lines.push(` return false;`);
146
+ lines.push(` }`);
147
+ lines.push('');
148
+ }
149
+ else {
150
+ // Unconditional forbid
151
+ lines.push(` // ${policy.desc ?? 'Forbid rule'}`);
152
+ lines.push(` return false;`);
153
+ return lines.join('\n');
154
+ }
155
+ }
156
+ // 2. Permit rules
157
+ const permits = relevantPolicies.filter(p => p.effect === 'permit');
158
+ for (const policy of permits) {
159
+ if (policy.when) {
160
+ if (!hasRecord && whenUsesRecord(policy.when)) {
161
+ // Skip record-dependent conditions for collection methods
162
+ continue;
163
+ }
164
+ const condition = whenToPhp(policy.when, schema, reader);
165
+ lines.push(` // ${policy.desc ?? 'Permit rule'}`);
166
+ lines.push(` if (${condition}) {`);
167
+ lines.push(` return true;`);
168
+ lines.push(` }`);
169
+ lines.push('');
170
+ }
171
+ else {
172
+ // Unconditional permit
173
+ lines.push(` // ${policy.desc ?? 'Permit rule'}`);
174
+ lines.push(` return true;`);
175
+ return lines.join('\n');
176
+ }
177
+ }
178
+ // 3. Default deny
179
+ lines.push(` return false;`);
180
+ return lines.join('\n');
181
+ }
182
+ // ============================================================================
183
+ // Method builder
184
+ // ============================================================================
185
+ function buildMethod(methodName, modelName, hasRecord, body) {
186
+ const recordParam = hasRecord ? `, ${modelName} $record` : '';
187
+ const returnType = 'bool';
188
+ return `
189
+ /**
190
+ * Determine whether the user can ${methodName}.
191
+ */
192
+ public function ${methodName}(User $user${recordParam}): ${returnType}
193
+ {
194
+ ${body}
195
+ }`;
196
+ }
197
+ // ============================================================================
198
+ // Condition → PHP translation
199
+ // ============================================================================
200
+ /** Convert a PolicyWhen (single or AND group) to a PHP expression. */
201
+ function whenToPhp(when, schema, reader) {
202
+ if (isAndCondition(when)) {
203
+ const parts = when.conditions.map(c => conditionToPhp(c, schema, reader));
204
+ return parts.map(p => `(${p})`).join(' && ');
205
+ }
206
+ return conditionToPhp(when, schema, reader);
207
+ }
208
+ /** Type guard for AND condition groups. */
209
+ function isAndCondition(when) {
210
+ return when.operator === 'and' && Array.isArray(when.conditions);
211
+ }
212
+ /** Convert a single condition to a PHP expression. */
213
+ export function conditionToPhp(cond, schema, reader) {
214
+ const { operator, left, right } = cond;
215
+ // Handle "in" / "not in" operators
216
+ if (operator === 'in' || operator === 'not in') {
217
+ return handleInOperator(operator, left, right, schema, reader);
218
+ }
219
+ // Standard comparison operators
220
+ const phpOp = mapOperator(operator);
221
+ const leftExpr = operandToPhp(left, schema, reader, 'left');
222
+ const rightExpr = operandToPhp(right, schema, reader, 'right');
223
+ return `${leftExpr} ${phpOp} ${rightExpr}`;
224
+ }
225
+ /** Map Cedar operator to PHP operator. */
226
+ function mapOperator(op) {
227
+ switch (op) {
228
+ case '=': return '===';
229
+ case '!=': return '!==';
230
+ case '>': return '>';
231
+ case '<': return '<';
232
+ case '>=': return '>=';
233
+ case '<=': return '<=';
234
+ default: return op;
235
+ }
236
+ }
237
+ /** Convert a condition operand to a PHP expression. */
238
+ function operandToPhp(operand, schema, reader, side) {
239
+ switch (operand.type) {
240
+ case 'property': {
241
+ const propName = operand.name;
242
+ // Check if it's an association (ManyToOne) → use _id
243
+ const prop = schema.properties?.[propName];
244
+ if (prop?.relation === 'ManyToOne') {
245
+ return `$record->${toSnakeCase(propName)}_id`;
246
+ }
247
+ return `$record->${toSnakeCase(propName)}`;
248
+ }
249
+ case 'variable': {
250
+ const varName = operand.name;
251
+ if (varName === '$user') {
252
+ return '$user->id';
253
+ }
254
+ if (varName === '$now') {
255
+ return 'now()';
256
+ }
257
+ if (varName === '$ip') {
258
+ return "request()->ip()";
259
+ }
260
+ // $user.attribute
261
+ if (varName.startsWith('$user.')) {
262
+ const attr = varName.slice('$user.'.length);
263
+ return `$user->${toSnakeCase(attr)}`;
264
+ }
265
+ return varName;
266
+ }
267
+ case 'literal': {
268
+ const val = operand.value;
269
+ if (typeof val === 'string') {
270
+ return `'${val}'`;
271
+ }
272
+ if (typeof val === 'number') {
273
+ return String(val);
274
+ }
275
+ if (typeof val === 'boolean') {
276
+ return val ? 'true' : 'false';
277
+ }
278
+ return String(val);
279
+ }
280
+ case 'set': {
281
+ const items = operand.items ?? [];
282
+ const phpItems = items.map(i => `'${i}'`).join(', ');
283
+ return `[${phpItems}]`;
284
+ }
285
+ case 'cidr': {
286
+ return `'${operand.name}'`;
287
+ }
288
+ case 'path': {
289
+ // Through-association: e.g. "org.members"
290
+ const pathName = operand.name;
291
+ const parts = pathName.split('.');
292
+ // $record->org->members()
293
+ const chain = parts.slice(0, -1).map(p => toSnakeCase(p)).join('->');
294
+ const lastRel = toCamelCase(parts[parts.length - 1]);
295
+ return `$record->${chain}->${lastRel}()`;
296
+ }
297
+ default:
298
+ return `/* unknown operand type: ${operand.type} */`;
299
+ }
300
+ }
301
+ /** Handle "in" and "not in" operators with various operand types. */
302
+ function handleInOperator(operator, left, right, schema, reader) {
303
+ const isNotIn = operator === 'not in';
304
+ // $user in members (ManyToMany)
305
+ if (left.type === 'variable' && left.name === '$user' && right.type === 'property') {
306
+ const relName = toCamelCase(right.name);
307
+ const expr = `$record->${relName}()->where('users.id', $user->id)->exists()`;
308
+ return isNotIn ? `!${expr}` : expr;
309
+ }
310
+ // $user in org.members (through-association)
311
+ if (left.type === 'variable' && left.name === '$user' && right.type === 'path') {
312
+ const pathName = right.name;
313
+ const parts = pathName.split('.');
314
+ const chain = parts.slice(0, -1).map(p => toCamelCase(p)).join('->');
315
+ const lastRel = toCamelCase(parts[parts.length - 1]);
316
+ const expr = `$record->${chain}->${lastRel}()->where('users.id', $user->id)->exists()`;
317
+ return isNotIn ? `!${expr}` : expr;
318
+ }
319
+ // $ip in cidr(...)
320
+ if (left.type === 'variable' && left.name === '$ip' && right.type === 'cidr') {
321
+ const expr = `$this->ipInCidr(request()->ip(), '${right.name}')`;
322
+ return isNotIn ? `!${expr}` : expr;
323
+ }
324
+ // property in set / property not in set
325
+ if (left.type === 'property' && right.type === 'set') {
326
+ const propExpr = operandToPhp(left, schema, reader, 'left');
327
+ const items = right.items ?? [];
328
+ const phpItems = items.map(i => `'${i}'`).join(', ');
329
+ if (isNotIn) {
330
+ return `!in_array(${propExpr}, [${phpItems}])`;
331
+ }
332
+ return `in_array(${propExpr}, [${phpItems}])`;
333
+ }
334
+ // Fallback: generic in
335
+ const leftExpr = operandToPhp(left, schema, reader, 'left');
336
+ const rightExpr = operandToPhp(right, schema, reader, 'right');
337
+ if (isNotIn) {
338
+ return `!in_array(${leftExpr}, ${rightExpr})`;
339
+ }
340
+ return `in_array(${leftExpr}, ${rightExpr})`;
341
+ }
342
+ // ============================================================================
343
+ // Helpers
344
+ // ============================================================================
345
+ /** Check if a when clause uses $record (property or path operands). */
346
+ function whenUsesRecord(when) {
347
+ if (isAndCondition(when)) {
348
+ return when.conditions.some(conditionUsesRecord);
349
+ }
350
+ return conditionUsesRecord(when);
351
+ }
352
+ /** Check if a single condition references $record. */
353
+ function conditionUsesRecord(cond) {
354
+ return operandUsesRecord(cond.left) || operandUsesRecord(cond.right);
355
+ }
356
+ /** Check if an operand references $record (property or path type). */
357
+ function operandUsesRecord(operand) {
358
+ return operand.type === 'property' || operand.type === 'path';
359
+ }
360
+ /** Expand wildcard (*) actions in policies. */
361
+ function expandWildcards(policies) {
362
+ return policies.map(p => {
363
+ if (p.actions.includes('*')) {
364
+ return { ...p, actions: ALL_ACTIONS };
365
+ }
366
+ return p;
367
+ });
368
+ }
369
+ /** Check if any policy uses CIDR conditions. */
370
+ function policiesUseCidr(policies) {
371
+ for (const p of policies) {
372
+ if (!p.when)
373
+ continue;
374
+ if (isAndCondition(p.when)) {
375
+ if (p.when.conditions.some(c => c.left.type === 'cidr' || c.right.type === 'cidr'))
376
+ return true;
377
+ }
378
+ else {
379
+ const cond = p.when;
380
+ if (cond.left.type === 'cidr' || cond.right.type === 'cidr')
381
+ return true;
382
+ }
383
+ }
384
+ return false;
385
+ }
386
+ /** Build the ipInCidr helper method. */
387
+ function buildCidrHelper() {
388
+ return `
389
+
390
+ /**
391
+ * Check if an IP address is within a CIDR range.
392
+ */
393
+ protected function ipInCidr(?string $ip, string $cidr): bool
394
+ {
395
+ if ($ip === null) {
396
+ return false;
397
+ }
398
+
399
+ [$subnet, $bits] = explode('/', $cidr);
400
+ $subnet = ip2long($subnet);
401
+ $ip = ip2long($ip);
402
+ $mask = -1 << (32 - (int) $bits);
403
+
404
+ return ($ip & $mask) === ($subnet & $mask);
405
+ }`;
406
+ }
@@ -38,7 +38,7 @@ export function buildRelation(propName, property, modelNamespace, context) {
38
38
  /** Generate pivot table name matching Go's GeneratePivotTableName logic. */
39
39
  export function generatePivotTableName(sourceTable, targetTable) {
40
40
  const tables = [singularize(sourceTable), singularize(targetTable)].sort();
41
- return tables.join('_');
41
+ return tables.join('_') + '_pivot';
42
42
  }
43
43
  /** Get the return type hint for a relation. */
44
44
  export function getReturnType(relation) {
@@ -14,6 +14,17 @@ export function generateServiceProvider(reader, config) {
14
14
  }
15
15
  morphEntries.sort();
16
16
  const morphMapContent = morphEntries.join('\n');
17
+ // Build package migration loading lines
18
+ const packageMigrationLines = [];
19
+ const packages = reader.getPackages();
20
+ for (const [name, pkg] of Object.entries(packages)) {
21
+ if (pkg.migrationsPath) {
22
+ packageMigrationLines.push(` $this->loadMigrationsFrom(base_path('${pkg.migrationsPath}')); // ${name}`);
23
+ }
24
+ }
25
+ const packageMigrationsBlock = packageMigrationLines.length > 0
26
+ ? `\n // Load package migrations\n${packageMigrationLines.join('\n')}\n`
27
+ : '';
17
28
  const providerNamespace = config.providers.namespace;
18
29
  const content = `<?php
19
30
 
@@ -50,7 +61,7 @@ class OmnifyServiceProvider extends ServiceProvider
50
61
  {
51
62
  // Load Omnify migrations from custom directory
52
63
  $this->loadMigrationsFrom(database_path('migrations/omnify'));
53
-
64
+ ${packageMigrationsBlock}
54
65
  // Register morph map for polymorphic relationships
55
66
  Relation::enforceMorphMap([
56
67
  ${morphMapContent}
@@ -24,6 +24,7 @@ export interface LaravelCodegenOverrides {
24
24
  resource?: LaravelPathOverride;
25
25
  factory?: LaravelPathOverride;
26
26
  provider?: LaravelPathOverride;
27
+ policy?: LaravelPathOverride;
27
28
  }
28
29
  /** PHP codegen configuration (resolved with defaults). */
29
30
  export interface PhpConfig {
@@ -53,6 +54,12 @@ export interface PhpConfig {
53
54
  namespace: string;
54
55
  path: string;
55
56
  };
57
+ policies: {
58
+ namespace: string;
59
+ baseNamespace: string;
60
+ path: string;
61
+ basePath: string;
62
+ };
56
63
  }
57
64
  /**
58
65
  * Derive full PHP config from optional overrides.
package/dist/php/types.js CHANGED
@@ -22,6 +22,7 @@ const DEFAULT_REQUEST_PATH = 'app/Http/Requests';
22
22
  const DEFAULT_RESOURCE_PATH = 'app/Http/Resources';
23
23
  const DEFAULT_FACTORY_PATH = 'database/factories';
24
24
  const DEFAULT_PROVIDER_PATH = 'app/Providers';
25
+ const DEFAULT_POLICY_PATH = 'app/Policies/Omnify';
25
26
  /**
26
27
  * Derive full PHP config from optional overrides.
27
28
  * All paths and namespaces fall back to sensible defaults.
@@ -37,6 +38,8 @@ export function derivePhpConfig(overrides) {
37
38
  const factoryNs = overrides?.factory?.namespace ?? pathToNamespace(factoryPath);
38
39
  const providerPath = overrides?.provider?.path ?? DEFAULT_PROVIDER_PATH;
39
40
  const providerNs = overrides?.provider?.namespace ?? pathToNamespace(providerPath);
41
+ const policyPath = overrides?.policy?.path ?? DEFAULT_POLICY_PATH;
42
+ const policyNs = overrides?.policy?.namespace ?? pathToNamespace(policyPath);
40
43
  return {
41
44
  models: {
42
45
  namespace: modelNs,
@@ -64,5 +67,11 @@ export function derivePhpConfig(overrides) {
64
67
  namespace: providerNs,
65
68
  path: providerPath,
66
69
  },
70
+ policies: {
71
+ namespace: policyNs,
72
+ baseNamespace: `${policyNs}\\Base`,
73
+ path: policyPath,
74
+ basePath: `${policyPath}/Base`,
75
+ },
67
76
  };
68
77
  }
package/dist/types.d.ts CHANGED
@@ -27,6 +27,7 @@ export interface SchemasJson {
27
27
  }
28
28
  /** Package metadata in schemas.json (codegen namespace references). */
29
29
  export interface PackageExportInfo {
30
+ readonly migrationsPath?: string;
30
31
  readonly codegen?: PackageCodegenExport;
31
32
  }
32
33
  /** Codegen namespace info from a package. */
@@ -105,6 +106,36 @@ export interface SchemaDefinition {
105
106
  readonly expandedProperties?: Record<string, ExpandedProperty>;
106
107
  readonly values?: readonly EnumValueDefinition[];
107
108
  readonly pivotFor?: readonly string[];
109
+ readonly policies?: readonly PolicyDefinition[];
110
+ }
111
+ /** Operand type in a policy condition. */
112
+ export type OperandType = 'property' | 'variable' | 'literal' | 'cidr' | 'set' | 'path';
113
+ /** One side of a condition expression. */
114
+ export interface ConditionOperand {
115
+ readonly type: OperandType;
116
+ readonly name?: string;
117
+ readonly value?: string | number | boolean;
118
+ readonly items?: readonly string[];
119
+ }
120
+ /** A parsed binary condition expression. */
121
+ export interface SingleCondition {
122
+ readonly operator: string;
123
+ readonly left: ConditionOperand;
124
+ readonly right: ConditionOperand;
125
+ }
126
+ /** AND group of conditions. */
127
+ export interface PolicyConditionAnd {
128
+ readonly operator: 'and';
129
+ readonly conditions: readonly SingleCondition[];
130
+ }
131
+ /** Policy when clause — either a single condition or AND group. */
132
+ export type PolicyWhen = SingleCondition | PolicyConditionAnd;
133
+ /** A single policy rule (permit or forbid). */
134
+ export interface PolicyDefinition {
135
+ readonly effect: 'permit' | 'forbid';
136
+ readonly actions: readonly string[];
137
+ readonly when?: PolicyWhen;
138
+ readonly desc?: string;
108
139
  }
109
140
  /** Application-level validation rules. Single source of truth for both Laravel validation and Zod schemas. */
110
141
  export interface ValidationRules {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",