@stacksjs/api 0.75.255 → 0.75.256
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/generate-openapi.js +1 -1
- package/package.json +5 -5
package/dist/generate-openapi.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{path}from"@stacksjs/path";export function ruleToSchema(rule){if(!rule||typeof rule!=="object")return{type:"string"};const r=rule,rules=Array.isArray(r.rules)?r.rules:[],named=(name)=>rules.find((entry)=>entry.name===name),parameter=(name,key)=>{const value=named(name)?.params?.[key];return typeof value==="number"?value:void 0},type=r.name??r._type??"string",out={type:type==="enum"?"string":type},min=parameter("min","min")??(typeof r._min==="number"?r._min:void 0),max=parameter("max","max")??(typeof r._max==="number"?r._max:void 0);if(min!==void 0)out[type==="string"?"minLength":"minimum"]=min;if(max!==void 0)out[type==="string"?"maxLength":"maximum"]=max;if(named("email")||r._email)out.format="email";if(named("url")||r._url)out.format="uri";if(named("uuid")||r._uuid)out.format="uuid";const allowed=Array.isArray(r.allowedValues)?r.allowedValues:Array.isArray(r._enum)?r._enum:null;if(allowed)out.enum=allowed;return out}export function ruleIsRequired(rule){if(!rule||typeof rule!=="object")return!1;const r=rule;return Boolean(r.isRequired??r._required)}function snakeCase(value){return value.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}export function modelDefinitionToSchema(model){const properties={id:{type:"integer"}},required=["id"];if(model.traits?.useUuid){properties.uuid={type:"string",format:"uuid"};required.push("uuid")}for(const[name,attribute]of Object.entries(model.attributes??{})){if(attribute.hidden===!0)continue;const key=snakeCase(name);properties[key]=ruleToSchema(attribute.validation?.rule);if(attribute.required===!0)required.push(key)}for(const relation of model.belongsTo??[]){if(typeof relation!=="string"||!relation.trim())continue;const key=`${snakeCase(relation.trim())}_id`;properties[key]??={type:"integer"}}if(model.traits?.useTimestamps!==!1){properties.created_at={type:"string",format:"date-time"};properties.updated_at={type:"string",format:"date-time"}}if(model.traits?.useSoftDeletes)properties.deleted_at={type:"string",format:"date-time",nullable:!0};return{type:"object",properties,required:[...new Set(required)]}}function modelApiPath(model){const api=model.traits?.useApi;if(!api||typeof api!=="object"||!api.uri)return null;const prefix=String(api.prefix||"").replace(/^\/+|\/+$/g,""),uri=String(api.uri).replace(/^\/+|\/+$/g,"");return`/api/${prefix?`${prefix}/`:""}${uri}`}export function describedResponses(action,base){const responses={...base},documented=action?.responses;if(documented&&typeof documented==="object")for(const[status,answer]of Object.entries(documented)){const shape=answer;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;responses[String(status)]={description:shape.description,...shape.schema&&typeof shape.schema==="object"?{content:{"application/json":{schema:shape.schema}}}:{}}}const headers=action?.responseHeaders;if(headers&&typeof headers==="object"){const described={};for(const[name,header]of Object.entries(headers)){const shape=header;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;described[name]={description:shape.description,...shape.schema&&typeof shape.schema==="object"?{schema:shape.schema}:{}}}if(Object.keys(described).length>0)for(const status of Object.keys(responses))responses[status]={...responses[status],headers:described}}return responses}async function loadAction(handlerPath){if(typeof handlerPath!=="string"||!handlerPath||handlerPath.includes(".."))return null;try{const{path:p}=await import("@stacksjs/path"),candidates=[p.appPath(`${handlerPath}.ts`),p.storagePath(`framework/defaults/app/${handlerPath}.ts`)];for(const candidate of candidates)try{if(!await Bun.file(candidate).exists())continue;const action=(await import(candidate)).default;return action&&typeof action==="object"?action:null}catch{}}catch{}return null}export async function generateOpenApi(options={}){const portable=options.portable??!0,{listRegisteredRoutes}=await import("@stacksjs/router"),pinnedEnv=portable?process.env.APP_ENV:void 0,hadPinnedEnv=portable&&"APP_ENV"in process.env;if(portable)process.env.APP_ENV="production";try{try{const{appRouteRegistry,loadRoutes}=await import("@stacksjs/router");await loadRoutes(await appRouteRegistry())}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load routes/api.ts + framework routes: ${message}`);console.warn(`[generateOpenApi] Failed to load routes/api.ts + framework routes: ${message}`)}try{const ormRoutesPackage="@stacksjs/orm/routes";try{await import(ormRoutesPackage)}catch{await import("../../orm/routes")}}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load useApi-generated ORM routes: ${message}`);console.warn(`[generateOpenApi] Failed to load useApi-generated ORM routes: ${message}`)}}finally{if(portable)if(hadPinnedEnv)process.env.APP_ENV=pinnedEnv;else delete process.env.APP_ENV}let modelRegistry={};try{const registryPackage="@stacksjs/orm/model-registry";let registryModule;try{registryModule=await import(registryPackage)}catch{registryModule=await import("../../orm/src/model-registry")}const registry=await registryModule.loadModelRegistryWithOrigins({defaultsRoot:path.storagePath("framework/defaults/app/Models"),userRoot:path.projectPath("app/Models")});let securityApi;try{securityApi=(await import(path.projectPath("config/security.ts"))).default?.api}catch{}const selection=registryModule.resolveModelApiSelection(securityApi?.models,process.env.STACKS_MODEL_APIS);modelRegistry=registryModule.selectApiModels(registry,selection).models}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit model-free schemas: ${message}`);console.warn(`[generateOpenApi] Failed to load model schemas: ${message}`)}const toOasPath=(p)=>p.replace(/(^|\/):(\w+)(?=$|\/)/g,"$1{$2}"),routes=[...listRegisteredRoutes()].map((route)=>({route,oasPath:toOasPath(route.path),method:route.method.toLowerCase()})).sort((a,b)=>{if(a.oasPath!==b.oasPath)return a.oasPath<b.oasPath?-1:1;return a.method<b.method?-1:a.method>b.method?1:0});if(portable&&routes.length===0)throw Error("[generateOpenApi] Refusing to emit an empty spec: no routes were registered. This usually means route loading failed silently rather than that the app has no API.");let appName="Stacks API";if(!portable)try{const{config}=await import("@stacksjs/config");if(config.app?.name)appName=`${config.app.name} API`}catch{}const spec={openapi:"3.0.0",info:{title:appName,version:"1.0.0"},paths:{},components:{schemas:{}}},modelApis=Object.values(modelRegistry).map((model)=>({name:String(model.name),path:modelApiPath(model),schema:modelDefinitionToSchema(model)})).filter((entry)=>entry.path!==null).sort((left,right)=>left.name<right.name?-1:left.name>right.name?1:0);for(const model of modelApis)spec.components.schemas[model.name]=model.schema;const operationIds=new Set;for(const{route:r,oasPath,method}of routes){if(!spec.paths[oasPath])spec.paths[oasPath]={};const paramNames=[];for(const m of r.path.matchAll(/\{(\w+)\}/g))if(m[1])paramNames.push(m[1]);for(const m of r.path.matchAll(/(?:^|\/):(\w+)(?=$|\/)/g))if(m[1])paramNames.push(m[1]);const preferredOperationId=r.name??`${method}_${oasPath.replace(/[^a-z0-9]/gi,"_")}`;let operationId=preferredOperationId;if(operationIds.has(operationId)){operationId=`${preferredOperationId}_${method}`;let suffix=2;while(operationIds.has(operationId))operationId=`${preferredOperationId}_${method}_${suffix++}`}operationIds.add(operationId);const op={summary:r.name??r.path,operationId,parameters:paramNames.map((name)=>({name,in:"path",required:!0,schema:{type:"string"}})),responses:{200:{description:"Successful response",content:{"application/json":{schema:{type:"object"}}}},422:{description:"Validation failed"},500:{description:"Server error"}}},modelApi=modelApis.find((model)=>oasPath===model.path||oasPath===`${model.path}/{id}`);if(modelApi){const dataSchema=oasPath===modelApi.path&&method==="get"?{type:"array",items:modelApi.schema}:modelApi.schema;op.responses[200].content={"application/json":{schema:{type:"object",properties:{data:dataSchema},required:["data"]}}};if(method==="post"&&oasPath===modelApi.path){op.responses[201]={...op.responses[200],description:"Created"};delete op.responses[200]}}const action=r.action??(r.handler?await loadAction(r.handler):null),validations=action&&typeof action.validations==="object"&&action.validations!==null?action.validations:null;op.responses=describedResponses(action,op.responses);const requestHeaders=action?.requestHeaders;if(requestHeaders&&typeof requestHeaders==="object")for(const[name,header]of Object.entries(requestHeaders)){const shape=header;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;op.parameters?.push({name,in:"header",description:shape.description,required:shape.required===!0,schema:shape.schema&&typeof shape.schema==="object"?shape.schema:{type:"string"}})}if(validations&&Object.keys(validations).length>0){const fields=Object.entries(validations).map(([field,def])=>{const rule=def?.rule;return{field,schema:ruleToSchema(rule),required:ruleIsRequired(rule)}});if(method==="get"||method==="head"||method==="delete"){const taken=new Set(paramNames);for(const{field,schema,required}of fields){if(taken.has(field))continue;op.parameters?.push({name:field,in:"query",required,schema})}}else{const schema={type:"object",properties:{},required:[]},{properties:props,required}=schema;for(const field of fields){props[field.field]=field.schema;if(field.required)required.push(field.field)}if(required.length===0)delete schema.required;op.requestBody={required:required.length>0,content:{"application/json":{schema}}}}}spec.paths[oasPath][method]=op}if(options.write!==!1){await Bun.write(path.frameworkPath("api/openapi.json"),JSON.stringify(spec,null,2));const{renderOpenApiTypes}=await import("./generate-types"),{renderApiClient}=await import("./generate-client");await Bun.write(path.frameworkPath("api/api-types.ts"),renderOpenApiTypes(spec));await Bun.write(path.frameworkPath("api/client.ts"),renderApiClient(spec,{name:spec.info.title}))}return spec}
|
|
1
|
+
import{path}from"@stacksjs/path";export function ruleToSchema(rule){if(!rule||typeof rule!=="object")return{type:"string"};const r=rule,rules=Array.isArray(r.rules)?r.rules:[],named=(name)=>rules.find((entry)=>entry.name===name),parameter=(name,key)=>{const value=named(name)?.params?.[key];return typeof value==="number"?value:void 0},type=r.name??r._type??"string",out={type:type==="enum"?"string":type},min=parameter("min","min")??(typeof r._min==="number"?r._min:void 0),max=parameter("max","max")??(typeof r._max==="number"?r._max:void 0);if(min!==void 0)out[type==="string"?"minLength":"minimum"]=min;if(max!==void 0)out[type==="string"?"maxLength":"maximum"]=max;if(named("email")||r._email)out.format="email";if(named("url")||r._url)out.format="uri";if(named("uuid")||r._uuid)out.format="uuid";const allowed=Array.isArray(r.allowedValues)?r.allowedValues:Array.isArray(r._enum)?r._enum:null;if(allowed)out.enum=allowed;return out}export function ruleIsRequired(rule){if(!rule||typeof rule!=="object")return!1;const r=rule;return Boolean(r.isRequired??r._required)}function snakeCase(value){return value.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}export function modelDefinitionToSchema(model){const properties={id:{type:"integer"}},required=["id"];if(model.traits?.useUuid){properties.uuid={type:"string",format:"uuid"};required.push("uuid")}for(const[name,attribute]of Object.entries(model.attributes??{})){if(attribute.hidden===!0)continue;const key=snakeCase(name);properties[key]=ruleToSchema(attribute.validation?.rule);if(attribute.required===!0)required.push(key)}for(const relation of model.belongsTo??[]){if(typeof relation!=="string"||!relation.trim())continue;const key=`${snakeCase(relation.trim())}_id`;properties[key]??={type:"integer"}}if(model.traits?.useTimestamps!==!1){properties.created_at={type:"string",format:"date-time"};properties.updated_at={type:"string",format:"date-time"}}if(model.traits?.useSoftDeletes)properties.deleted_at={type:"string",format:"date-time",nullable:!0};return{type:"object",properties,required:[...new Set(required)]}}function modelApiPath(model){const api=model.traits?.useApi;if(!api||typeof api!=="object"||!api.uri)return null;const prefix=String(api.prefix||"").replace(/^\/+|\/+$/g,""),uri=String(api.uri).replace(/^\/+|\/+$/g,"");return`/api/${prefix?`${prefix}/`:""}${uri}`}export function describedResponses(action,base){const responses={...base},documented=action?.responses;if(documented&&typeof documented==="object")for(const[status,answer]of Object.entries(documented)){const shape=answer;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;responses[String(status)]={description:shape.description,...shape.schema&&typeof shape.schema==="object"?{content:{"application/json":{schema:shape.schema}}}:{}}}const headers=action?.responseHeaders;if(headers&&typeof headers==="object"){const described={};for(const[name,header]of Object.entries(headers)){const shape=header;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;described[name]={description:shape.description,...shape.schema&&typeof shape.schema==="object"?{schema:shape.schema}:{}}}if(Object.keys(described).length>0)for(const status of Object.keys(responses))responses[status]={...responses[status],headers:described}}return responses}async function loadAction(handlerPath){if(typeof handlerPath!=="string"||!handlerPath||handlerPath.includes(".."))return null;try{const{path:p}=await import("@stacksjs/path"),candidates=[p.appPath(`${handlerPath}.ts`),p.defaultsPath(`app/${handlerPath}.ts`)];for(const candidate of candidates)try{if(!await Bun.file(candidate).exists())continue;const action=(await import(candidate)).default;return action&&typeof action==="object"?action:null}catch{}}catch{}return null}export async function generateOpenApi(options={}){const portable=options.portable??!0,{listRegisteredRoutes}=await import("@stacksjs/router"),pinnedEnv=portable?process.env.APP_ENV:void 0,hadPinnedEnv=portable&&"APP_ENV"in process.env;if(portable)process.env.APP_ENV="production";try{try{const{appRouteRegistry,loadRoutes}=await import("@stacksjs/router");await loadRoutes(await appRouteRegistry())}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load routes/api.ts + framework routes: ${message}`);console.warn(`[generateOpenApi] Failed to load routes/api.ts + framework routes: ${message}`)}try{const ormRoutesPackage="@stacksjs/orm/routes";try{await import(ormRoutesPackage)}catch{await import("../../orm/routes")}}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load useApi-generated ORM routes: ${message}`);console.warn(`[generateOpenApi] Failed to load useApi-generated ORM routes: ${message}`)}}finally{if(portable)if(hadPinnedEnv)process.env.APP_ENV=pinnedEnv;else delete process.env.APP_ENV}let modelRegistry={};try{const registryPackage="@stacksjs/orm/model-registry";let registryModule;try{registryModule=await import(registryPackage)}catch{registryModule=await import("../../orm/src/model-registry")}const registry=await registryModule.loadModelRegistryWithOrigins({defaultsRoot:path.defaultsPath("app/Models"),userRoot:path.projectPath("app/Models")});let securityApi;try{securityApi=(await import(path.projectPath("config/security.ts"))).default?.api}catch{}const selection=registryModule.resolveModelApiSelection(securityApi?.models,process.env.STACKS_MODEL_APIS);modelRegistry=registryModule.selectApiModels(registry,selection).models}catch(error){const message=error instanceof Error?error.message:String(error);if(portable)throw Error(`[generateOpenApi] Refusing to emit model-free schemas: ${message}`);console.warn(`[generateOpenApi] Failed to load model schemas: ${message}`)}const toOasPath=(p)=>p.replace(/(^|\/):(\w+)(?=$|\/)/g,"$1{$2}"),routes=[...listRegisteredRoutes()].map((route)=>({route,oasPath:toOasPath(route.path),method:route.method.toLowerCase()})).sort((a,b)=>{if(a.oasPath!==b.oasPath)return a.oasPath<b.oasPath?-1:1;return a.method<b.method?-1:a.method>b.method?1:0});if(portable&&routes.length===0)throw Error("[generateOpenApi] Refusing to emit an empty spec: no routes were registered. This usually means route loading failed silently rather than that the app has no API.");let appName="Stacks API";if(!portable)try{const{config}=await import("@stacksjs/config");if(config.app?.name)appName=`${config.app.name} API`}catch{}const spec={openapi:"3.0.0",info:{title:appName,version:"1.0.0"},paths:{},components:{schemas:{}}},modelApis=Object.values(modelRegistry).map((model)=>({name:String(model.name),path:modelApiPath(model),schema:modelDefinitionToSchema(model)})).filter((entry)=>entry.path!==null).sort((left,right)=>left.name<right.name?-1:left.name>right.name?1:0);for(const model of modelApis)spec.components.schemas[model.name]=model.schema;const operationIds=new Set;for(const{route:r,oasPath,method}of routes){if(!spec.paths[oasPath])spec.paths[oasPath]={};const paramNames=[];for(const m of r.path.matchAll(/\{(\w+)\}/g))if(m[1])paramNames.push(m[1]);for(const m of r.path.matchAll(/(?:^|\/):(\w+)(?=$|\/)/g))if(m[1])paramNames.push(m[1]);const preferredOperationId=r.name??`${method}_${oasPath.replace(/[^a-z0-9]/gi,"_")}`;let operationId=preferredOperationId;if(operationIds.has(operationId)){operationId=`${preferredOperationId}_${method}`;let suffix=2;while(operationIds.has(operationId))operationId=`${preferredOperationId}_${method}_${suffix++}`}operationIds.add(operationId);const op={summary:r.name??r.path,operationId,parameters:paramNames.map((name)=>({name,in:"path",required:!0,schema:{type:"string"}})),responses:{200:{description:"Successful response",content:{"application/json":{schema:{type:"object"}}}},422:{description:"Validation failed"},500:{description:"Server error"}}},modelApi=modelApis.find((model)=>oasPath===model.path||oasPath===`${model.path}/{id}`);if(modelApi){const dataSchema=oasPath===modelApi.path&&method==="get"?{type:"array",items:modelApi.schema}:modelApi.schema;op.responses[200].content={"application/json":{schema:{type:"object",properties:{data:dataSchema},required:["data"]}}};if(method==="post"&&oasPath===modelApi.path){op.responses[201]={...op.responses[200],description:"Created"};delete op.responses[200]}}const action=r.action??(r.handler?await loadAction(r.handler):null),validations=action&&typeof action.validations==="object"&&action.validations!==null?action.validations:null;op.responses=describedResponses(action,op.responses);const requestHeaders=action?.requestHeaders;if(requestHeaders&&typeof requestHeaders==="object")for(const[name,header]of Object.entries(requestHeaders)){const shape=header;if(!shape||typeof shape!=="object"||typeof shape.description!=="string")continue;op.parameters?.push({name,in:"header",description:shape.description,required:shape.required===!0,schema:shape.schema&&typeof shape.schema==="object"?shape.schema:{type:"string"}})}if(validations&&Object.keys(validations).length>0){const fields=Object.entries(validations).map(([field,def])=>{const rule=def?.rule;return{field,schema:ruleToSchema(rule),required:ruleIsRequired(rule)}});if(method==="get"||method==="head"||method==="delete"){const taken=new Set(paramNames);for(const{field,schema,required}of fields){if(taken.has(field))continue;op.parameters?.push({name:field,in:"query",required,schema})}}else{const schema={type:"object",properties:{},required:[]},{properties:props,required}=schema;for(const field of fields){props[field.field]=field.schema;if(field.required)required.push(field.field)}if(required.length===0)delete schema.required;op.requestBody={required:required.length>0,content:{"application/json":{schema}}}}}spec.paths[oasPath][method]=op}if(options.write!==!1){await Bun.write(path.frameworkPath("api/openapi.json"),JSON.stringify(spec,null,2));const{renderOpenApiTypes}=await import("./generate-types"),{renderApiClient}=await import("./generate-client");await Bun.write(path.frameworkPath("api/api-types.ts"),renderOpenApiTypes(spec));await Bun.write(path.frameworkPath("api/client.ts"),renderApiClient(spec,{name:spec.info.title}))}return spec}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/api",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.256",
|
|
6
6
|
"description": "The Stacks API utilities.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -61,15 +61,15 @@
|
|
|
61
61
|
"prepublishOnly": "bun run build"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stacksjs/config": "0.75.
|
|
65
|
-
"@stacksjs/path": "0.75.
|
|
64
|
+
"@stacksjs/config": "0.75.256",
|
|
65
|
+
"@stacksjs/path": "0.75.256"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@stacksjs/utils": "0.75.
|
|
68
|
+
"@stacksjs/utils": "0.75.256",
|
|
69
69
|
"better-dx": "^0.2.24"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@stacksjs/router": "0.75.
|
|
72
|
+
"@stacksjs/router": "0.75.256"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|
|
75
75
|
"@stacksjs/router": {
|