11import { lowerCaseFirst } from '@zenstackhq/common-helpers' ;
2- import type { AttributeApplication , EnumDef , FieldDef , ModelDef , SchemaDef , TypeDefDef } from '@zenstackhq/orm/schema' ;
2+ import type { EnumDef , FieldDef , ModelDef , SchemaDef , TypeDefDef } from '@zenstackhq/orm/schema' ;
33import type { OpenAPIV3_1 } from 'openapi-types' ;
44import { PROCEDURE_ROUTE_PREFIXES } from '../common/procedures' ;
55import {
6+ DEFAULT_SPEC_TITLE ,
7+ DEFAULT_SPEC_VERSION ,
68 getIncludedModels ,
79 getMetaDescription ,
810 isFieldOmitted ,
911 isFilterKindIncluded ,
1012 isModelIncluded ,
1113 isOperationIncluded ,
1214 isProcedureIncluded ,
15+ mayDenyAccess ,
1316} from '../common/spec-utils' ;
1417import type { OpenApiSpecOptions } from '../common/types' ;
1518import type { RestApiHandlerOptions } from '.' ;
@@ -70,8 +73,8 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
7073 return {
7174 openapi : '3.1.0' ,
7275 info : {
73- title : options ?. title ?? 'ZenStack Generated API' ,
74- version : options ?. version ?? '1.0.0' ,
76+ title : options ?. title ?? DEFAULT_SPEC_TITLE ,
77+ version : options ?. version ?? DEFAULT_SPEC_VERSION ,
7578 ...( options ?. description && { description : options . description } ) ,
7679 ...( options ?. summary && { summary : options . summary } ) ,
7780 } ,
@@ -229,7 +232,7 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
229232 } ,
230233 } ,
231234 '400' : ERROR_400 ,
232- ...( this . mayDenyAccess ( modelDef , 'create' ) && { '403' : ERROR_403 } ) ,
235+ ...( mayDenyAccess ( modelDef , 'create' , this . specOptions ?. respectAccessPolicies ) && { '403' : ERROR_403 } ) ,
233236 '422' : ERROR_422 ,
234237 } ,
235238 } ;
@@ -293,7 +296,7 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
293296 } ,
294297 } ,
295298 '400' : ERROR_400 ,
296- ...( this . mayDenyAccess ( modelDef , 'update' ) && { '403' : ERROR_403 } ) ,
299+ ...( mayDenyAccess ( modelDef , 'update' , this . specOptions ?. respectAccessPolicies ) && { '403' : ERROR_403 } ) ,
297300 '404' : ERROR_404 ,
298301 '422' : ERROR_422 ,
299302 } ,
@@ -308,7 +311,7 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
308311 parameters : [ idParam ] ,
309312 responses : {
310313 '200' : { description : 'Deleted successfully' } ,
311- ...( this . mayDenyAccess ( modelDef , 'delete' ) && { '403' : ERROR_403 } ) ,
314+ ...( mayDenyAccess ( modelDef , 'delete' , this . specOptions ?. respectAccessPolicies ) && { '403' : ERROR_403 } ) ,
312315 '404' : ERROR_404 ,
313316 } ,
314317 } ;
@@ -359,7 +362,7 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
359362 } ;
360363
361364 if ( this . nestedRoutes && relModelDef ) {
362- const mayDeny = this . mayDenyAccess ( relModelDef , isCollection ? 'create' : 'update' ) ;
365+ const mayDeny = mayDenyAccess ( relModelDef , isCollection ? 'create' : 'update' , this . specOptions ?. respectAccessPolicies ) ;
363366 if ( isCollection && isOperationIncluded ( fieldDef . type , 'create' , this . queryOptions ) ) {
364367 // POST /{model}/{id}/{field} — nested create
365368 pathItem [ 'post' ] = {
@@ -434,8 +437,8 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
434437 ) : Record < string , any > {
435438 const childIdParam = { name : 'childId' , in : 'path' , required : true , schema : { type : 'string' } } ;
436439 const idParam = { $ref : '#/components/parameters/id' } ;
437- const mayDenyUpdate = this . mayDenyAccess ( relModelDef , 'update' ) ;
438- const mayDenyDelete = this . mayDenyAccess ( relModelDef , 'delete' ) ;
440+ const mayDenyUpdate = mayDenyAccess ( relModelDef , 'update' , this . specOptions ?. respectAccessPolicies ) ;
441+ const mayDenyDelete = mayDenyAccess ( relModelDef , 'delete' , this . specOptions ?. respectAccessPolicies ) ;
439442 const result : Record < string , any > = { } ;
440443
441444 if ( isOperationIncluded ( fieldDef . type , 'findUnique' , this . queryOptions ) ) {
@@ -523,7 +526,7 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
523526 ? { $ref : '#/components/schemas/_toManyRelationshipRequest' }
524527 : { $ref : '#/components/schemas/_toOneRelationshipRequest' } ;
525528
526- const mayDeny = this . mayDenyAccess ( modelDef , 'update' ) ;
529+ const mayDeny = mayDenyAccess ( modelDef , 'update' , this . specOptions ?. respectAccessPolicies ) ;
527530
528531 const pathItem : Record < string , any > = {
529532 get : {
@@ -1204,50 +1207,4 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
12041207 return modelDef . idFields . map ( ( name ) => modelDef . fields [ name ] ) . filter ( ( f ) : f is FieldDef => f !== undefined ) ;
12051208 }
12061209
1207- /**
1208- * Checks if an operation on a model may be denied by access policies.
1209- * Returns true when `respectAccessPolicies` is enabled and the model's
1210- * policies for the given operation are NOT a constant allow (i.e., not
1211- * simply `@@allow('...', true)` with no `@@deny` rules).
1212- */
1213- private mayDenyAccess ( modelDef : ModelDef , operation : string ) : boolean {
1214- if ( ! this . specOptions ?. respectAccessPolicies ) return false ;
1215-
1216- const policyAttrs = ( modelDef . attributes ?? [ ] ) . filter (
1217- ( attr ) => attr . name === '@@allow' || attr . name === '@@deny' ,
1218- ) ;
1219-
1220- // No policy rules at all means default-deny
1221- if ( policyAttrs . length === 0 ) return true ;
1222-
1223- const getArgByName = ( args : AttributeApplication [ 'args' ] , name : string ) =>
1224- args ?. find ( ( a ) => a . name === name ) ?. value ;
1225-
1226- const matchesOperation = ( args : AttributeApplication [ 'args' ] ) => {
1227- const val = getArgByName ( args , 'operation' ) ;
1228- if ( ! val || val . kind !== 'literal' || typeof val . value !== 'string' ) return false ;
1229- const ops = val . value . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ;
1230- return ops . includes ( operation ) || ops . includes ( 'all' ) ;
1231- } ;
1232-
1233- const hasEffectiveDeny = policyAttrs . some ( ( attr ) => {
1234- if ( attr . name !== '@@deny' || ! matchesOperation ( attr . args ) ) return false ;
1235- const condition = getArgByName ( attr . args , 'condition' ) ;
1236- // @@deny ('op', false) is a no-op — skip it
1237- return ! ( condition ?. kind === 'literal' && condition . value === false ) ;
1238- } ) ;
1239- if ( hasEffectiveDeny ) return true ;
1240-
1241- const relevantAllow = policyAttrs . filter (
1242- ( attr ) => attr . name === '@@allow' && matchesOperation ( attr . args ) ,
1243- ) ;
1244-
1245- // If any allow rule has a constant `true` condition (and no deny), access is unconditional
1246- const hasConstantAllow = relevantAllow . some ( ( attr ) => {
1247- const condition = getArgByName ( attr . args , 'condition' ) ;
1248- return condition ?. kind === 'literal' && condition . value === true ;
1249- } ) ;
1250-
1251- return ! hasConstantAllow ;
1252- }
12531210}
0 commit comments