@stacksjs/api 0.75.190 → 0.75.192

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 @@
1
+ class FetcherResponseImpl{data;status;headers;isOk;constructor(data,status,headers,isOk){this.data=data;this.status=status;this.headers=headers;this.isOk=isOk}ok(){return this.status===200}created(){return this.status===201}accepted(){return this.status===202}noContent(){return this.status===204}movedPermanently(){return this.status===301}found(){return this.status===302}badRequest(){return this.status===400}unauthorized(){return this.status===401}paymentRequired(){return this.status===402}forbidden(){return this.status===403}notFound(){return this.status===404}requestTimeout(){return this.status===408}conflict(){return this.status===409}unprocessableEntity(){return this.status===422}tooManyRequests(){return this.status===429}serverError(){return this.status===500}}class Fetcher{defaultHeaders={Accept:"application/json","Content-Type":"application/json"};queryParams;bodyData;isFormData=!1;customHeaders={};attachments=[];isMultipart=!1;digestAuth;withQueryParams(params){this.queryParams=params;return this}withHeaders(headers){this.customHeaders={...this.customHeaders,...headers};return this}accept(contentType){this.defaultHeaders.Accept=contentType;return this}acceptJson(){return this.accept("application/json")}withToken(token){this.customHeaders.Authorization=`Bearer ${token}`;return this}withBasicAuth(username,password){const credentials=btoa(`${username}:${password}`);this.customHeaders.Authorization=`Basic ${credentials}`;return this}withDigestAuth(username,password){this.digestAuth={username,password};return this}withBody(data){this.bodyData=data;return this}asForm(){this.isFormData=!0;this.defaultHeaders["Content-Type"]="application/x-www-form-urlencoded";return this}attach(name,content,filename,headers){const blobContent=typeof content==="string"?new Blob([content],{type:headers?.["Content-Type"]||"text/plain"}):content;this.attachments.push({name,content:blobContent,filename,headers});this.isMultipart=!0;delete this.defaultHeaders["Content-Type"];return this}getHeaders(){return{...this.defaultHeaders,...this.customHeaders}}addQueryParams(url){if(!this.queryParams)return url;const searchParams=new URLSearchParams;Object.entries(this.queryParams).forEach(([key,value])=>{if(value!=null)searchParams.append(key,String(value))});const queryString=searchParams.toString();if(!queryString)return url;const hasParams=url.includes("?");return url+(hasParams?"&":"?")+queryString}async formatBody(body){const finalBody=body||this.bodyData;if(this.isMultipart){const formData=new FormData;if(finalBody)Object.entries(finalBody).forEach(([key,value])=>{formData.append(key,String(value))});for(const attachment of this.attachments)if(attachment.headers){const blob=attachment.content instanceof Blob?new Blob([attachment.content],{type:attachment.headers["Content-Type"]}):attachment.content;formData.append(attachment.name,blob,attachment.filename)}else formData.append(attachment.name,attachment.content,attachment.filename);return formData}if(this.isFormData&&finalBody){const formData=new URLSearchParams;Object.entries(finalBody).forEach(([key,value])=>{if(value!=null)formData.append(key,String(value))});return formData.toString()}return finalBody?JSON.stringify(finalBody):void 0}resetState(){this.queryParams=void 0;this.bodyData=void 0;this.isFormData=!1;this.isMultipart=!1;this.customHeaders={};this.attachments=[];this.digestAuth=void 0;this.defaultHeaders["Content-Type"]="application/json"}async handleDigestAuth(response,url,method,body){if(response.status!==401||!this.digestAuth)return response;const authHeader=response.headers.get("WWW-Authenticate");if(!authHeader?.startsWith("Digest "))return response;const challenge=authHeader.substring(7).split(",").reduce((acc,part)=>{const[key,value]=part.trim().split("=");if(key)acc[key]=value?.replace(/["']/g,"")??"";return acc},{}),ha1=await this.generateMD5(`${this.digestAuth.username}:${challenge.realm}:${this.digestAuth.password}`),ha2=await this.generateMD5(`${method}:${url}`),nonceCount="00000001",cnonce=await this.generateMD5(Date.now().toString()).then((hash)=>hash.substring(0,8)),response_value=await this.generateMD5(`${ha1}:${challenge.nonce}:${nonceCount}:${cnonce}:${challenge.qop}:${ha2}`),digestResponse={username:`"${this.digestAuth.username}"`,realm:`"${challenge.realm}"`,nonce:`"${challenge.nonce}"`,uri:`"${url}"`,algorithm:challenge.algorithm?`"${challenge.algorithm}"`:void 0,qop:`"${challenge.qop}"`,nc:nonceCount,cnonce:`"${cnonce}"`,response:`"${response_value}"`,opaque:challenge.opaque?`"${challenge.opaque}"`:void 0},authValue=Object.entries(digestResponse).filter(([_,value])=>value!==void 0).map(([key,value])=>`${key}=${value}`).join(", ");this.customHeaders.Authorization=`Digest ${authValue}`;return fetch(url,{method,headers:this.getHeaders(),body:body?await this.formatBody(body):void 0})}async generateMD5(str){const data=new TextEncoder().encode(str),hashBuffer=await crypto.subtle.digest("SHA-256",data);return Array.from(new Uint8Array(hashBuffer)).map((b)=>b.toString(16).padStart(2,"0")).join("")}async request(url,method,body){const urlWithParams=this.addQueryParams(url),formattedBody=await this.formatBody(body);let response=await fetch(urlWithParams,{method,headers:this.getHeaders(),body:formattedBody});if(this.digestAuth)response=await this.handleDigestAuth(response,urlWithParams,method,body);const data=await response.json();this.resetState();return new FetcherResponseImpl(data,response.status,response.headers,response.ok)}async get(url){return this.request(url,"GET")}async post(url,body){return this.request(url,"POST",body)}async put(url,body){return this.request(url,"PUT",body)}async patch(url,body){return this.request(url,"PATCH",body)}async delete(url){return this.request(url,"DELETE")}}export const fetcher=new Fetcher;
@@ -0,0 +1,108 @@
1
+ import{readFileSync,writeFileSync}from"node:fs";import{resolve}from"node:path";import{schemaType}from"./generate-types";const METHODS=["get","put","post","delete","options","head","patch"];export function methodName(operationId){const parts=operationId.replace(/[{}]/g,"").split(/[^a-z0-9]+/i).filter(Boolean),trimmed=parts[0]?.toLowerCase()==="api"?parts.slice(1):parts[1]?.toLowerCase()==="api"?[parts[0],...parts.slice(2)]:parts,[first,...rest]=trimmed;if(!first)return"call";const head=first.charAt(0).toLowerCase()+first.slice(1),tail=rest.map((part)=>part.charAt(0).toUpperCase()+part.slice(1)),name=[head,...tail].join("");return/^[a-z_$]/i.test(name)?name:`call${name.charAt(0).toUpperCase()}${name.slice(1)}`}function successType(operation){const responses=operation.responses??{},status=Object.keys(responses).find((code)=>code.startsWith("2"));if(!status)return"unknown";const content=responses[status]?.content,json=content?.["application/json"]??Object.values(content??{})[0];return json?.schema?schemaType(json.schema):"unknown"}function comment(lines,indent=" "){const kept=lines.filter(Boolean);if(!kept.length)return"";return`${indent}/**
2
+ ${kept.map((line)=>`${indent} * ${line}`).join(`
3
+ `)}
4
+ ${indent} */
5
+ `}function renderOperation(route,method,operation,name){const parameters=operation.parameters??[],pathParams=parameters.filter((parameter)=>parameter.in==="path"),queryParams=parameters.filter((parameter)=>parameter.in==="query"),bodySchema=operation.requestBody?.content?.["application/json"]?.schema,bodyRequired=Boolean(operation.requestBody?.required),fields=[];for(const parameter of[...pathParams,...queryParams]){const required=parameter.in==="path"||parameter.required;fields.push(`${JSON.stringify(parameter.name)}${required?"":"?"}: ${schemaType(parameter.schema)}`)}if(bodySchema)fields.push(`body${bodyRequired?"":"?"}: ${schemaType(bodySchema)}`);const everyFieldOptional=[...pathParams,...queryParams].every((p)=>p.in!=="path"&&!p.required)&&(!bodySchema||!bodyRequired),argument=fields.length?`input${everyFieldOptional?"?":""}: { ${fields.join("; ")} }, `:"",queryList=queryParams.length?`[${queryParams.map((parameter)=>JSON.stringify(parameter.name)).join(", ")}]`:"[]";return`${comment([operation.summary&&operation.summary!==route?operation.summary:`${method.toUpperCase()} ${route}`,operation.description??""])} ${name}(${argument}options?: RequestOptions): Promise<ApiResult<${successType(operation)}>> {
6
+ return request(config, ${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(route)}, ${fields.length?"input ?? {}":"{}"}, ${queryList}, ${bodySchema?"true":"false"}, options)
7
+ },`}export function renderApiClient(document,options={}){const title=options.name??"API",used=new Map,methods=[];for(const[route,item]of Object.entries(document.paths??{}))for(const method of METHODS){const operation=item[method];if(!operation)continue;const preferred=methodName(operation.operationId??`${method}_${route}`),seen=used.get(preferred)??0;used.set(preferred,seen+1);methods.push(renderOperation(route,method,operation,seen===0?preferred:`${preferred}${seen+1}`))}return`/* eslint-disable */
8
+ /**
9
+ * ${title} client, generated from the OpenAPI document.
10
+ *
11
+ * Do not edit. Run \`buddy generate:openapi\` to rebuild it from the routes.
12
+ *
13
+ * No imports and no dependencies: copy this file anywhere that has \`fetch\`.
14
+ */
15
+
16
+ /** Every call answers with one of these. Nothing here throws for a 4xx. */
17
+ export type ApiResult<T> =
18
+ | { ok: true, status: number, data: T, headers: Headers }
19
+ | { ok: false, status: number, error: unknown, headers: Headers }
20
+
21
+ export interface ClientConfig {
22
+ /** Where the API lives, e.g. \`https://example.com\`. No trailing slash needed. */
23
+ baseUrl: string
24
+ /** Sent as \`Authorization: Bearer \u2026\` when present. */
25
+ token?: string
26
+ /** Extra headers on every request. */
27
+ headers?: Record<string, string>
28
+ /** Swap in a different fetch - a test double, or one that retries. */
29
+ fetch?: typeof fetch
30
+ }
31
+
32
+ export interface RequestOptions {
33
+ /** Abort in flight. */
34
+ signal?: AbortSignal
35
+ /** Headers for this call only, merged over the client's. */
36
+ headers?: Record<string, string>
37
+ }
38
+
39
+ function buildUrl(config: ClientConfig, route: string, input: Record<string, unknown>, query: string[]): string {
40
+ // Path parameters are substituted, never appended: a \`{id}\` left in the URL
41
+ // is a 404 whose message is about the literal string "{id}".
42
+ const filled = route.replace(/\\{(\\w+)\\}/g, (_, key: string) => encodeURIComponent(String(input[key] ?? '')))
43
+ const url = new URL(filled.replace(/^\\/+/, '/'), config.baseUrl.endsWith('/') ? config.baseUrl : \`\${config.baseUrl}/\`)
44
+
45
+ for (const key of query) {
46
+ const value = input[key]
47
+ // Absent means absent. Sending \`?path=\` asks for the file called empty
48
+ // string, which is a different question from not filtering.
49
+ if (value === undefined || value === null) continue
50
+ url.searchParams.set(key, String(value))
51
+ }
52
+
53
+ return url.toString()
54
+ }
55
+
56
+ async function request<T>(
57
+ config: ClientConfig,
58
+ method: string,
59
+ route: string,
60
+ input: Record<string, unknown>,
61
+ query: string[],
62
+ hasBody: boolean,
63
+ options?: RequestOptions,
64
+ ): Promise<ApiResult<T>> {
65
+ const call = config.fetch ?? fetch
66
+ const url = buildUrl(config, route, input, query)
67
+
68
+ const headers: Record<string, string> = {
69
+ Accept: 'application/json',
70
+ ...(config.headers ?? {}),
71
+ ...(options?.headers ?? {}),
72
+ }
73
+ if (config.token) headers.Authorization = \`Bearer \${config.token}\`
74
+
75
+ const body = hasBody && input.body !== undefined ? JSON.stringify(input.body) : undefined
76
+ if (body !== undefined) headers['Content-Type'] = 'application/json'
77
+
78
+ const response = await call(url, { method, headers, body, signal: options?.signal })
79
+
80
+ const text = await response.text()
81
+ let parsed: unknown = text
82
+ if (text.length) {
83
+ // A non-JSON body is kept verbatim rather than replaced with a parse error.
84
+ // An endpoint that answered with HTML is worth seeing.
85
+ try { parsed = JSON.parse(text) } catch { parsed = text }
86
+ }
87
+ else {
88
+ parsed = null
89
+ }
90
+
91
+ return response.ok
92
+ ? { ok: true, status: response.status, data: parsed as T, headers: response.headers }
93
+ : { ok: false, status: response.status, error: parsed, headers: response.headers }
94
+ }
95
+
96
+ export function createClient(config: ClientConfig) {
97
+ return {
98
+ /** The configuration in use, so a caller can rebuild a variant of it. */
99
+ config,
100
+
101
+ ${methods.join(`
102
+
103
+ `)}
104
+ }
105
+ }
106
+
107
+ export type Client = ReturnType<typeof createClient>
108
+ `}if(import.meta.main){const apiRoot=resolve(import.meta.dir,"../../../api"),input=resolve(process.argv[2]||resolve(apiRoot,"openapi.json")),output=resolve(process.argv[3]||resolve(apiRoot,"client.ts")),document=JSON.parse(readFileSync(input,"utf8"));writeFileSync(output,renderApiClient(document));console.log(`Generated ${output}`)}
@@ -0,0 +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}
@@ -0,0 +1,31 @@
1
+ import{readFileSync,writeFileSync}from"node:fs";import{resolve}from"node:path";const methods=["get","put","post","delete","options","head","patch","trace"];function literal(value){return typeof value==="string"?JSON.stringify(value):String(value)}export function schemaType(schema){if(!schema)return"unknown";if(schema.enum?.length)return schema.enum.map(literal).join(" | ");if(schema.type==="string")return"string";if(schema.type==="number"||schema.type==="integer")return"number";if(schema.type==="boolean")return"boolean";if(schema.type==="null")return"null";if(schema.type==="array")return`Array<${schemaType(schema.items)}>`;if(schema.type==="object"||schema.properties||schema.additionalProperties){const required=new Set(schema.required||[]),fields=Object.entries(schema.properties||{}).map(([name,value])=>`${JSON.stringify(name)}${required.has(name)?"":"?"}: ${schemaType(value)}`);if(schema.additionalProperties){const value=schema.additionalProperties===!0?"unknown":schemaType(schema.additionalProperties);fields.push(`[key: string]: ${value}`)}return fields.length?`{ ${fields.join("; ")} }`:"Record<string, unknown>"}return"unknown"}function parameterGroups(parameters=[]){return` parameters: {
2
+ ${["query","header","path","cookie"].map((location)=>{const entries=parameters.filter((parameter)=>parameter.in===location);if(!entries.length)return` ${location}?: never`;const fields=entries.map((parameter)=>`${JSON.stringify(parameter.name)}${parameter.required?"":"?"}: ${schemaType(parameter.schema)}`);return` ${location}${entries.every((parameter)=>!parameter.required)?"?":""}: { ${fields.join("; ")} }`}).join(`
3
+ `)}
4
+ }`}function contentTypes(content){if(!content||!Object.keys(content).length)return"never";return`{ ${Object.entries(content).map(([type,value])=>`${JSON.stringify(type)}: ${schemaType(value.schema)}`).join("; ")} }`}function renderOperation(operation){const requestBody=operation.requestBody?`
5
+ requestBody${operation.requestBody.required?"":"?"}: { content: ${contentTypes(operation.requestBody.content)} }`:`
6
+ requestBody?: never`,responses=Object.entries(operation.responses||{}).map(([status,response])=>` ${JSON.stringify(status)}: { content: ${contentTypes(response.content)} }`);return`{
7
+ ${parameterGroups(operation.parameters)}${requestBody}
8
+ responses: {
9
+ ${responses.join(`
10
+ `)}
11
+ }
12
+ }`}export function renderOpenApiTypes(document){const paths=Object.entries(document.paths||{}).map(([route,item])=>{const operations=methods.map((method)=>{const operation=item[method];return operation?` ${method}: ${renderOperation(operation)}`:` ${method}?: never`});return` ${JSON.stringify(route)}: {
13
+ ${operations.join(`
14
+ `)}
15
+ }`}),schemas=Object.entries(document.components?.schemas||{}).map(([name,schema])=>` ${JSON.stringify(name)}: ${schemaType(schema)}`);return`/**
16
+ * Generated from storage/framework/api/openapi.json by @stacksjs/api.
17
+ * Do not edit this file directly; run bun run docs:artifacts to regenerate it.
18
+ */
19
+
20
+ export interface paths {
21
+ ${paths.join(`
22
+ `)}
23
+ }
24
+
25
+ export interface components {
26
+ schemas: {
27
+ ${schemas.join(`
28
+ `)}
29
+ }
30
+ }
31
+ `}if(import.meta.main){const apiRoot=resolve(import.meta.dir,"../../../api"),input=resolve(process.argv[2]||resolve(apiRoot,"openapi.json")),output=resolve(process.argv[3]||resolve(apiRoot,"api-types.ts")),document=JSON.parse(readFileSync(input,"utf8"));writeFileSync(output,renderOpenApiTypes(document));console.log(`Generated ${output}`)}
package/dist/index.js CHANGED
@@ -1,139 +1 @@
1
- // @bun
2
- var ee=Object.defineProperty;var te=(e)=>e;function ne(e,t){this[e]=te.bind(null,t)}var re=(e,t)=>{for(var n in t)ee(e,n,{get:t[n],enumerable:!0,configurable:!0,set:ne.bind(t,n)})};var T=(e,t,n)=>()=>{if(e)try{t=e(e=0)}catch(r){n=[r]}if(n)throw n[0];return t};function oe(e){return typeof e==="string"?JSON.stringify(e):String(e)}function O(e){if(!e)return"unknown";if(e.enum?.length)return e.enum.map(oe).join(" | ");if(e.type==="string")return"string";if(e.type==="number"||e.type==="integer")return"number";if(e.type==="boolean")return"boolean";if(e.type==="null")return"null";if(e.type==="array")return`Array<${O(e.items)}>`;if(e.type==="object"||e.properties||e.additionalProperties){let t=new Set(e.required||[]),n=Object.entries(e.properties||{}).map(([r,o])=>`${JSON.stringify(r)}${t.has(r)?"":"?"}: ${O(o)}`);if(e.additionalProperties){let r=e.additionalProperties===!0?"unknown":O(e.additionalProperties);n.push(`[key: string]: ${r}`)}return n.length?`{ ${n.join("; ")} }`:"Record<string, unknown>"}return"unknown"}function ie(e=[]){return` parameters: {
3
- ${["query","header","path","cookie"].map((n)=>{let r=e.filter((s)=>s.in===n);if(!r.length)return` ${n}?: never`;let o=r.map((s)=>`${JSON.stringify(s.name)}${s.required?"":"?"}: ${O(s.schema)}`);return` ${n}${r.every((s)=>!s.required)?"?":""}: { ${o.join("; ")} }`}).join(`
4
- `)}
5
- }`}function J(e){if(!e||!Object.keys(e).length)return"never";return`{ ${Object.entries(e).map(([t,n])=>`${JSON.stringify(t)}: ${O(n.schema)}`).join("; ")} }`}function ae(e){let t=e.requestBody?`
6
- requestBody${e.requestBody.required?"":"?"}: { content: ${J(e.requestBody.content)} }`:`
7
- requestBody?: never`,n=Object.entries(e.responses||{}).map(([r,o])=>` ${JSON.stringify(r)}: { content: ${J(o.content)} }`);return`{
8
- ${ie(e.parameters)}${t}
9
- responses: {
10
- ${n.join(`
11
- `)}
12
- }
13
- }`}function F(e){let t=Object.entries(e.paths||{}).map(([r,o])=>{let s=se.map((a)=>{let c=o[a];return c?` ${a}: ${ae(c)}`:` ${a}?: never`});return` ${JSON.stringify(r)}: {
14
- ${s.join(`
15
- `)}
16
- }`}),n=Object.entries(e.components?.schemas||{}).map(([r,o])=>` ${JSON.stringify(r)}: ${O(o)}`);return`/**
17
- * Generated from storage/framework/api/openapi.json by @stacksjs/api.
18
- * Do not edit this file directly; run bun run docs:artifacts to regenerate it.
19
- */
20
-
21
- export interface paths {
22
- ${t.join(`
23
- `)}
24
- }
25
-
26
- export interface components {
27
- schemas: {
28
- ${n.join(`
29
- `)}
30
- }
31
- }
32
- `}var se;var D=T(()=>{se=["get","put","post","delete","options","head","patch","trace"]});function ue(e){let t=e.replace(/[{}]/g,"").split(/[^a-z0-9]+/i).filter(Boolean),n=t[0]?.toLowerCase()==="api"?t.slice(1):t[1]?.toLowerCase()==="api"?[t[0],...t.slice(2)]:t,[r,...o]=n;if(!r)return"call";let s=r.charAt(0).toLowerCase()+r.slice(1),a=o.map((p)=>p.charAt(0).toUpperCase()+p.slice(1)),c=[s,...a].join("");return/^[a-z_$]/i.test(c)?c:`call${c.charAt(0).toUpperCase()}${c.slice(1)}`}function pe(e){let t=e.responses??{},n=Object.keys(t).find((s)=>s.startsWith("2"));if(!n)return"unknown";let r=t[n]?.content,o=r?.["application/json"]??Object.values(r??{})[0];return o?.schema?O(o.schema):"unknown"}function de(e,t=" "){let n=e.filter(Boolean);if(!n.length)return"";return`${t}/**
33
- ${n.map((r)=>`${t} * ${r}`).join(`
34
- `)}
35
- ${t} */
36
- `}function le(e,t,n,r){let o=n.parameters??[],s=o.filter((d)=>d.in==="path"),a=o.filter((d)=>d.in==="query"),c=n.requestBody?.content?.["application/json"]?.schema,p=Boolean(n.requestBody?.required),l=[];for(let d of[...s,...a]){let g=d.in==="path"||d.required;l.push(`${JSON.stringify(d.name)}${g?"":"?"}: ${O(d.schema)}`)}if(c)l.push(`body${p?"":"?"}: ${O(c)}`);let m=[...s,...a].every((d)=>d.in!=="path"&&!d.required)&&(!c||!p),y=l.length?`input${m?"?":""}: { ${l.join("; ")} }, `:"",i=a.length?`[${a.map((d)=>JSON.stringify(d.name)).join(", ")}]`:"[]";return`${de([n.summary&&n.summary!==e?n.summary:`${t.toUpperCase()} ${e}`,n.description??""])} ${r}(${y}options?: RequestOptions): Promise<ApiResult<${pe(n)}>> {
37
- return request(config, ${JSON.stringify(t.toUpperCase())}, ${JSON.stringify(e)}, ${l.length?"input ?? {}":"{}"}, ${i}, ${c?"true":"false"}, options)
38
- },`}function H(e,t={}){let n=t.name??"API",r=new Map,o=[];for(let[s,a]of Object.entries(e.paths??{}))for(let c of ce){let p=a[c];if(!p)continue;let l=ue(p.operationId??`${c}_${s}`),m=r.get(l)??0;r.set(l,m+1),o.push(le(s,c,p,m===0?l:`${l}${m+1}`))}return`/* eslint-disable */
39
- /**
40
- * ${n} client, generated from the OpenAPI document.
41
- *
42
- * Do not edit. Run \`buddy generate:openapi\` to rebuild it from the routes.
43
- *
44
- * No imports and no dependencies: copy this file anywhere that has \`fetch\`.
45
- */
46
-
47
- /** Every call answers with one of these. Nothing here throws for a 4xx. */
48
- export type ApiResult<T> =
49
- | { ok: true, status: number, data: T, headers: Headers }
50
- | { ok: false, status: number, error: unknown, headers: Headers }
51
-
52
- export interface ClientConfig {
53
- /** Where the API lives, e.g. \`https://example.com\`. No trailing slash needed. */
54
- baseUrl: string
55
- /** Sent as \`Authorization: Bearer \u2026\` when present. */
56
- token?: string
57
- /** Extra headers on every request. */
58
- headers?: Record<string, string>
59
- /** Swap in a different fetch - a test double, or one that retries. */
60
- fetch?: typeof fetch
61
- }
62
-
63
- export interface RequestOptions {
64
- /** Abort in flight. */
65
- signal?: AbortSignal
66
- /** Headers for this call only, merged over the client's. */
67
- headers?: Record<string, string>
68
- }
69
-
70
- function buildUrl(config: ClientConfig, route: string, input: Record<string, unknown>, query: string[]): string {
71
- // Path parameters are substituted, never appended: a \`{id}\` left in the URL
72
- // is a 404 whose message is about the literal string "{id}".
73
- const filled = route.replace(/\\{(\\w+)\\}/g, (_, key: string) => encodeURIComponent(String(input[key] ?? '')))
74
- const url = new URL(filled.replace(/^\\/+/, '/'), config.baseUrl.endsWith('/') ? config.baseUrl : \`\${config.baseUrl}/\`)
75
-
76
- for (const key of query) {
77
- const value = input[key]
78
- // Absent means absent. Sending \`?path=\` asks for the file called empty
79
- // string, which is a different question from not filtering.
80
- if (value === undefined || value === null) continue
81
- url.searchParams.set(key, String(value))
82
- }
83
-
84
- return url.toString()
85
- }
86
-
87
- async function request<T>(
88
- config: ClientConfig,
89
- method: string,
90
- route: string,
91
- input: Record<string, unknown>,
92
- query: string[],
93
- hasBody: boolean,
94
- options?: RequestOptions,
95
- ): Promise<ApiResult<T>> {
96
- const call = config.fetch ?? fetch
97
- const url = buildUrl(config, route, input, query)
98
-
99
- const headers: Record<string, string> = {
100
- Accept: 'application/json',
101
- ...(config.headers ?? {}),
102
- ...(options?.headers ?? {}),
103
- }
104
- if (config.token) headers.Authorization = \`Bearer \${config.token}\`
105
-
106
- const body = hasBody && input.body !== undefined ? JSON.stringify(input.body) : undefined
107
- if (body !== undefined) headers['Content-Type'] = 'application/json'
108
-
109
- const response = await call(url, { method, headers, body, signal: options?.signal })
110
-
111
- const text = await response.text()
112
- let parsed: unknown = text
113
- if (text.length) {
114
- // A non-JSON body is kept verbatim rather than replaced with a parse error.
115
- // An endpoint that answered with HTML is worth seeing.
116
- try { parsed = JSON.parse(text) } catch { parsed = text }
117
- }
118
- else {
119
- parsed = null
120
- }
121
-
122
- return response.ok
123
- ? { ok: true, status: response.status, data: parsed as T, headers: response.headers }
124
- : { ok: false, status: response.status, error: parsed, headers: response.headers }
125
- }
126
-
127
- export function createClient(config: ClientConfig) {
128
- return {
129
- /** The configuration in use, so a caller can rebuild a variant of it. */
130
- config,
131
-
132
- ${o.join(`
133
-
134
- `)}
135
- }
136
- }
137
-
138
- export type Client = ReturnType<typeof createClient>
139
- `}var ce;var B=T(()=>{D();ce=["get","put","post","delete","options","head","patch"]});var L=()=>{};var z={};re(z,{loadModelRegistry:()=>ge,loadModelRegistryWithOrigins:()=>W,modelFiles:()=>V,resolveModelApiSelection:()=>ye,selectApiModels:()=>Re});import{readdirSync as he}from"fs";import{basename as fe,extname as U,join as me}from"path";function V(e){let t=[],n=(r)=>{let o;try{o=he(r,{withFileTypes:!0})}catch{return}for(let s of o){let a=me(r,s.name);if(s.isDirectory()){n(a);continue}if(![".ts",".js"].includes(U(s.name)))continue;if(s.name.startsWith(".")||s.name.startsWith("index"))continue;t.push(a)}};return n(e),t.sort()}async function I(e,t,n,r){for(let o of V(e))try{let s=U(o),a=await import(`${o}?t=${Date.now()}`),c=a.default??a,p=c.name??fe(o,s);n.models[p]={...c,name:p},n.origins[p]=t}catch(s){r?.(o,s)}}async function W(e){let t={models:{},origins:{}};return await I(e.defaultsRoot,"default",t,e.onImportError),await I(e.userRoot,"app",t,e.onImportError),t}async function ge(e){return(await W(e)).models}function ye(e,t){let n,r="default";if(t!==void 0&&t!==null)n=t.split(","),r="env";else if(typeof e==="string")n=[e],r="config";else if(Array.isArray(e)&&e.every((s)=>typeof s==="string"))n=e,r="config";if(n===void 0)return{all:!0,own:!1,names:new Set,source:r};let o=n.map((s)=>s.trim().toLowerCase()).filter(Boolean);if(o.length===0||o.includes("none"))return{all:!1,own:!1,names:new Set,source:r};if(o.includes("all"))return{all:!0,own:!1,names:new Set,source:r};return{all:!1,own:o.includes("own"),names:new Set(o.filter((s)=>s!=="own")),source:r}}function Re(e,t){let n={},r=new Set;for(let[o,s]of Object.entries(e.models)){if(!s?.traits?.useApi)continue;let a=o.toLowerCase(),c=t.names.has(a);if(c)r.add(a);if(t.all||c||t.own&&e.origins[o]==="app")n[o]=s}return{models:n,unmatched:[...t.names].filter((o)=>!r.has(o)).sort()}}var Q=()=>{};class E{data;status;headers;isOk;constructor(e,t,n,r){this.data=e;this.status=t;this.headers=n;this.isOk=r}ok(){return this.status===200}created(){return this.status===201}accepted(){return this.status===202}noContent(){return this.status===204}movedPermanently(){return this.status===301}found(){return this.status===302}badRequest(){return this.status===400}unauthorized(){return this.status===401}paymentRequired(){return this.status===402}forbidden(){return this.status===403}notFound(){return this.status===404}requestTimeout(){return this.status===408}conflict(){return this.status===409}unprocessableEntity(){return this.status===422}tooManyRequests(){return this.status===429}serverError(){return this.status===500}}class N{defaultHeaders={Accept:"application/json","Content-Type":"application/json"};queryParams;bodyData;isFormData=!1;customHeaders={};attachments=[];isMultipart=!1;digestAuth;withQueryParams(e){return this.queryParams=e,this}withHeaders(e){return this.customHeaders={...this.customHeaders,...e},this}accept(e){return this.defaultHeaders.Accept=e,this}acceptJson(){return this.accept("application/json")}withToken(e){return this.customHeaders.Authorization=`Bearer ${e}`,this}withBasicAuth(e,t){let n=btoa(`${e}:${t}`);return this.customHeaders.Authorization=`Basic ${n}`,this}withDigestAuth(e,t){return this.digestAuth={username:e,password:t},this}withBody(e){return this.bodyData=e,this}asForm(){return this.isFormData=!0,this.defaultHeaders["Content-Type"]="application/x-www-form-urlencoded",this}attach(e,t,n,r){let o=typeof t==="string"?new Blob([t],{type:r?.["Content-Type"]||"text/plain"}):t;return this.attachments.push({name:e,content:o,filename:n,headers:r}),this.isMultipart=!0,delete this.defaultHeaders["Content-Type"],this}getHeaders(){return{...this.defaultHeaders,...this.customHeaders}}addQueryParams(e){if(!this.queryParams)return e;let t=new URLSearchParams;Object.entries(this.queryParams).forEach(([o,s])=>{if(s!=null)t.append(o,String(s))});let n=t.toString();if(!n)return e;let r=e.includes("?");return e+(r?"&":"?")+n}async formatBody(e){let t=e||this.bodyData;if(this.isMultipart){let n=new FormData;if(t)Object.entries(t).forEach(([r,o])=>{n.append(r,String(o))});for(let r of this.attachments)if(r.headers){let o=r.content instanceof Blob?new Blob([r.content],{type:r.headers["Content-Type"]}):r.content;n.append(r.name,o,r.filename)}else n.append(r.name,r.content,r.filename);return n}if(this.isFormData&&t){let n=new URLSearchParams;return Object.entries(t).forEach(([r,o])=>{if(o!=null)n.append(r,String(o))}),n.toString()}return t?JSON.stringify(t):void 0}resetState(){this.queryParams=void 0,this.bodyData=void 0,this.isFormData=!1,this.isMultipart=!1,this.customHeaders={},this.attachments=[],this.digestAuth=void 0,this.defaultHeaders["Content-Type"]="application/json"}async handleDigestAuth(e,t,n,r){if(e.status!==401||!this.digestAuth)return e;let o=e.headers.get("WWW-Authenticate");if(!o?.startsWith("Digest "))return e;let s=o.substring(7).split(",").reduce((u,d)=>{let[g,S]=d.trim().split("=");if(g)u[g]=S?.replace(/["']/g,"")??"";return u},{}),a=await this.generateMD5(`${this.digestAuth.username}:${s.realm}:${this.digestAuth.password}`),c=await this.generateMD5(`${n}:${t}`),p="00000001",l=await this.generateMD5(Date.now().toString()).then((u)=>u.substring(0,8)),m=await this.generateMD5(`${a}:${s.nonce}:${p}:${l}:${s.qop}:${c}`),y={username:`"${this.digestAuth.username}"`,realm:`"${s.realm}"`,nonce:`"${s.nonce}"`,uri:`"${t}"`,algorithm:s.algorithm?`"${s.algorithm}"`:void 0,qop:`"${s.qop}"`,nc:p,cnonce:`"${l}"`,response:`"${m}"`,opaque:s.opaque?`"${s.opaque}"`:void 0},i=Object.entries(y).filter(([u,d])=>d!==void 0).map(([u,d])=>`${u}=${d}`).join(", ");return this.customHeaders.Authorization=`Digest ${i}`,fetch(t,{method:n,headers:this.getHeaders(),body:r?await this.formatBody(r):void 0})}async generateMD5(e){let n=new TextEncoder().encode(e),r=await crypto.subtle.digest("SHA-256",n);return Array.from(new Uint8Array(r)).map((s)=>s.toString(16).padStart(2,"0")).join("")}async request(e,t,n){let r=this.addQueryParams(e),o=await this.formatBody(n),s=await fetch(r,{method:t,headers:this.getHeaders(),body:o});if(this.digestAuth)s=await this.handleDigestAuth(s,r,t,n);let a=await s.json();return this.resetState(),new E(a,s.status,s.headers,s.ok)}async get(e){return this.request(e,"GET")}async post(e,t){return this.request(e,"POST",t)}async put(e,t){return this.request(e,"PUT",t)}async patch(e,t){return this.request(e,"PATCH",t)}async delete(e){return this.request(e,"DELETE")}}var je=new N;B();import{path as P}from"@stacksjs/path";function Z(e){if(!e||typeof e!=="object")return{type:"string"};let t=e,n=Array.isArray(t.rules)?t.rules:[],r=(m)=>n.find((y)=>y.name===m),o=(m,y)=>{let i=r(m)?.params?.[y];return typeof i==="number"?i:void 0},s=t.name??t._type??"string",a={type:s==="enum"?"string":s},c=o("min","min")??(typeof t._min==="number"?t._min:void 0),p=o("max","max")??(typeof t._max==="number"?t._max:void 0);if(c!==void 0)a[s==="string"?"minLength":"minimum"]=c;if(p!==void 0)a[s==="string"?"maxLength":"maximum"]=p;if(r("email")||t._email)a.format="email";if(r("url")||t._url)a.format="uri";if(r("uuid")||t._uuid)a.format="uuid";let l=Array.isArray(t.allowedValues)?t.allowedValues:Array.isArray(t._enum)?t._enum:null;if(l)a.enum=l;return a}function be(e){if(!e||typeof e!=="object")return!1;let t=e;return Boolean(t.isRequired??t._required)}function G(e){return e.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}function we(e){let t={id:{type:"integer"}},n=["id"];if(e.traits?.useUuid)t.uuid={type:"string",format:"uuid"},n.push("uuid");for(let[r,o]of Object.entries(e.attributes??{})){if(o.hidden===!0)continue;let s=G(r);if(t[s]=Z(o.validation?.rule),o.required===!0)n.push(s)}for(let r of e.belongsTo??[]){if(typeof r!=="string"||!r.trim())continue;let o=`${G(r.trim())}_id`;t[o]??={type:"integer"}}if(e.traits?.useTimestamps!==!1)t.created_at={type:"string",format:"date-time"},t.updated_at={type:"string",format:"date-time"};if(e.traits?.useSoftDeletes)t.deleted_at={type:"string",format:"date-time",nullable:!0};return{type:"object",properties:t,required:[...new Set(n)]}}function Oe(e){let t=e.traits?.useApi;if(!t||typeof t!=="object"||!t.uri)return null;let n=String(t.prefix||"").replace(/^\/+|\/+$/g,""),r=String(t.uri).replace(/^\/+|\/+$/g,"");return`/api/${n?`${n}/`:""}${r}`}function Ae(e,t){let n={...t},r=e?.responses;if(r&&typeof r==="object")for(let[s,a]of Object.entries(r)){let c=a;if(!c||typeof c!=="object"||typeof c.description!=="string")continue;n[String(s)]={description:c.description,...c.schema&&typeof c.schema==="object"?{content:{"application/json":{schema:c.schema}}}:{}}}let o=e?.responseHeaders;if(o&&typeof o==="object"){let s={};for(let[a,c]of Object.entries(o)){let p=c;if(!p||typeof p!=="object"||typeof p.description!=="string")continue;s[a]={description:p.description,...p.schema&&typeof p.schema==="object"?{schema:p.schema}:{}}}if(Object.keys(s).length>0)for(let a of Object.keys(n))n[a]={...n[a],headers:s}}return n}async function $e(e){if(typeof e!=="string"||!e||e.includes(".."))return null;try{let{path:t}=await import("@stacksjs/path"),n=[t.appPath(`${e}.ts`),t.storagePath(`framework/defaults/app/${e}.ts`)];for(let r of n)try{if(!await Bun.file(r).exists())continue;let a=(await import(r)).default;return a&&typeof a==="object"?a:null}catch{}}catch{}return null}async function Be(e={}){let t=e.portable??!0,{listRegisteredRoutes:n}=await import("@stacksjs/router"),r=t?process.env.APP_ENV:void 0,o=t&&"APP_ENV"in process.env;if(t)process.env.APP_ENV="production";try{try{let{appRouteRegistry:i,loadRoutes:u}=await import("@stacksjs/router");await u(await i())}catch(i){let u=i instanceof Error?i.message:String(i);if(t)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load routes/api.ts + framework routes: ${u}`);console.warn(`[generateOpenApi] Failed to load routes/api.ts + framework routes: ${u}`)}try{try{await import("@stacksjs/orm/routes")}catch{await Promise.resolve().then(() => (L(),{}))}}catch(i){let u=i instanceof Error?i.message:String(i);if(t)throw Error(`[generateOpenApi] Refusing to emit a partial spec: failed to load useApi-generated ORM routes: ${u}`);console.warn(`[generateOpenApi] Failed to load useApi-generated ORM routes: ${u}`)}}finally{if(t)if(o)process.env.APP_ENV=r;else delete process.env.APP_ENV}let s={};try{let u;try{u=await import("@stacksjs/orm/model-registry")}catch{u=await Promise.resolve().then(() => (Q(),z))}let d=await u.loadModelRegistryWithOrigins({defaultsRoot:P.storagePath("framework/defaults/app/Models"),userRoot:P.projectPath("app/Models")}),g;try{g=(await import(P.projectPath("config/security.ts"))).default?.api}catch{}let S=u.resolveModelApiSelection(g?.models,process.env.STACKS_MODEL_APIS);s=u.selectApiModels(d,S).models}catch(i){let u=i instanceof Error?i.message:String(i);if(t)throw Error(`[generateOpenApi] Refusing to emit model-free schemas: ${u}`);console.warn(`[generateOpenApi] Failed to load model schemas: ${u}`)}let a=(i)=>i.replace(/(^|\/):(\w+)(?=$|\/)/g,"$1{$2}"),c=[...n()].map((i)=>({route:i,oasPath:a(i.path),method:i.method.toLowerCase()})).sort((i,u)=>{if(i.oasPath!==u.oasPath)return i.oasPath<u.oasPath?-1:1;return i.method<u.method?-1:i.method>u.method?1:0});if(t&&c.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 p="Stacks API";if(!t)try{let{config:i}=await import("@stacksjs/config");if(i.app?.name)p=`${i.app.name} API`}catch{}let l={openapi:"3.0.0",info:{title:p,version:"1.0.0"},paths:{},components:{schemas:{}}},m=Object.values(s).map((i)=>({name:String(i.name),path:Oe(i),schema:we(i)})).filter((i)=>i.path!==null).sort((i,u)=>i.name<u.name?-1:i.name>u.name?1:0);for(let i of m)l.components.schemas[i.name]=i.schema;let y=new Set;for(let{route:i,oasPath:u,method:d}of c){if(!l.paths[u])l.paths[u]={};let g=[];for(let h of i.path.matchAll(/\{(\w+)\}/g))if(h[1])g.push(h[1]);for(let h of i.path.matchAll(/(?:^|\/):(\w+)(?=$|\/)/g))if(h[1])g.push(h[1]);let S=i.name??`${d}_${u.replace(/[^a-z0-9]/gi,"_")}`,j=S;if(y.has(j)){j=`${S}_${d}`;let h=2;while(y.has(j))j=`${S}_${d}_${h++}`}y.add(j);let b={summary:i.name??i.path,operationId:j,parameters:g.map((h)=>({name:h,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"}}},x=m.find((h)=>u===h.path||u===`${h.path}/{id}`);if(x){let h=u===x.path&&d==="get"?{type:"array",items:x.schema}:x.schema;if(b.responses[200].content={"application/json":{schema:{type:"object",properties:{data:h},required:["data"]}}},d==="post"&&u===x.path)b.responses[201]={...b.responses[200],description:"Created"},delete b.responses[200]}let q=i.action??(i.handler?await $e(i.handler):null),C=q&&typeof q.validations==="object"&&q.validations!==null?q.validations:null;b.responses=Ae(q,b.responses);let M=q?.requestHeaders;if(M&&typeof M==="object")for(let[h,w]of Object.entries(M)){let f=w;if(!f||typeof f!=="object"||typeof f.description!=="string")continue;b.parameters?.push({name:h,in:"header",description:f.description,required:f.required===!0,schema:f.schema&&typeof f.schema==="object"?f.schema:{type:"string"}})}if(C&&Object.keys(C).length>0){let h=Object.entries(C).map(([w,f])=>{let A=f?.rule;return{field:w,schema:Z(A),required:be(A)}});if(d==="get"||d==="head"||d==="delete"){let w=new Set(g);for(let{field:f,schema:A,required:k}of h){if(w.has(f))continue;b.parameters?.push({name:f,in:"query",required:k,schema:A})}}else{let w={type:"object",properties:{},required:[]},{properties:f,required:A}=w;for(let k of h)if(f[k.field]=k.schema,k.required)A.push(k.field);if(A.length===0)delete w.required;b.requestBody={required:A.length>0,content:{"application/json":{schema:w}}}}}l.paths[u][d]=b}if(e.write!==!1){await Bun.write(P.frameworkPath("api/openapi.json"),JSON.stringify(l,null,2));await Promise.resolve().then(() => D());await Promise.resolve().then(() => B());await Bun.write(P.frameworkPath("api/api-types.ts"),F(l)),await Bun.write(P.frameworkPath("api/client.ts"),H(l,{name:l.info.title}))}return l}D();class R{static instance=new R;isMissing(){return!0}}class v{data;constructor(e){this.data=e}}class K{condition;value;defaultValue;constructor(e,t,n=new R){this.condition=e;this.value=t;this.defaultValue=n}resolve(){return(typeof this.condition==="function"?this.condition():this.condition)?this.value:this.defaultValue}}class X{resource;additional={};request;static wrap="data";static additionalMeta={};constructor(e){this.resource=e}withRequest(e){return this.request=e,this}withAdditional(e){return this.additional={...this.additional,...e},this}resolve(e){let t=this.toArray(e||this.request);return this.filterAndResolve(t)}toResponse(e){let t=this.resolve(e),n=this.constructor.wrap;if(n)return{[n]:t,...this.additional};return{...t,...this.additional}}toJson(e){return JSON.stringify(this.toResponse(e))}toJSON(){return this.toResponse()}static collection(e){return new _(e,this)}when(e,t,n){if(typeof e==="function"?e():e)return typeof t==="function"?t():t;return n!==void 0?n:R.instance}whenNotNull(e,t){if(e===null||e===void 0)return R.instance;return t?t(e):e}whenLoaded(e,t,n){let r=this.resource;if(!((e in r)&&r[e]!==void 0))return n!==void 0?n:R.instance;if(t!==void 0)return typeof t==="function"?t():t;return r[e]}whenCounted(e,t){let n=this.resource,r=`${e}_count`;if(r in n)return Number(n[r]);return t!==void 0?t:R.instance}merge(e){return new v(e)}mergeWhen(e,t){if(!(typeof e==="function"?e():e))return R.instance;let r=typeof t==="function"?t():t;return new v(r)}static MAX_RESOLVE_DEPTH=20;filterAndResolve(e,t=0){if(t>=X.MAX_RESOLVE_DEPTH)return e;let n={};for(let[r,o]of Object.entries(e)){if(o instanceof R)continue;if(o instanceof v){Object.assign(n,this.filterAndResolve(o.data,t+1));continue}if(o instanceof K){let s=o.resolve();if(!(s instanceof R))n[r]=s;continue}if(o!==null&&typeof o==="object"&&!Array.isArray(o)&&!(o instanceof Date)){n[r]=this.filterAndResolve(o,t+1);continue}if(Array.isArray(o)){n[r]=o.filter((s)=>!(s instanceof R)).map((s)=>{if(s instanceof v)return s.data;if(s!==null&&typeof s==="object"&&!(s instanceof Date))return this.filterAndResolve(s,t+1);return s});continue}n[r]=o}return n}static withoutWrapping(){this.wrap=null}static wrapWith(e){this.wrap=e}}class _{resources;ResourceClass;additional={};request;static wrap="data";constructor(e,t){this.resources=e,this.ResourceClass=t}withRequest(e){return this.request=e,this}withAdditional(e){return this.additional={...this.additional,...e},this}resolve(e){let t=e||this.request;return this.resources.map((n)=>{let r=new this.ResourceClass(n);if(t)r.withRequest(t);return r.resolve(t)})}toResponse(e){let t=this.resolve(e),n=this.constructor.wrap;if(n)return{[n]:t,...this.additional};return{data:t,...this.additional}}toJson(e){return JSON.stringify(this.toResponse(e))}count(){return this.resources.length}isEmpty(){return this.resources.length===0}isNotEmpty(){return this.resources.length>0}}class Y extends _{meta;links;constructor(e,t){super(e.data,t);this.meta=e.meta,this.links=e.links}static fromPagination(e,t,n){let{currentPage:r,perPage:o,total:s,baseUrl:a=""}=n,c=Math.ceil(s/o),p=s>0?(r-1)*o+1:null,l=s>0?Math.min(r*o,s):null;return new Y({data:e,meta:{current_page:r,from:p,last_page:c,per_page:o,to:l,total:s},links:{first:`${a}?page=1`,last:`${a}?page=${c}`,prev:r>1?`${a}?page=${r-1}`:null,next:r<c?`${a}?page=${r+1}`:null}},t)}toResponse(e){return{data:this.resolve(e),meta:this.meta,links:this.links,...this.additional}}}function Ee(e,t){let n=t(e);return{toResponse:()=>({data:n}),toJson:()=>JSON.stringify({data:n})}}function Ne(e,t){let n=e.map(t);return{toResponse:()=>({data:n}),toJson:()=>JSON.stringify({data:n})}}function Fe(e){return Object.freeze({async execute(t){return await e.authorize(t),e.handle(t)}})}export{K as ConditionalValue,X as JsonResource,v as MergeValue,R as MissingValue,Y as PaginatedResourceCollection,_ as ResourceCollection,Ne as collection,Fe as defineApiOperation,Ae as describedResponses,je as fetcher,Be as generateOpenApi,ue as methodName,we as modelDefinitionToSchema,H as renderApiClient,F as renderOpenApiTypes,Ee as resource,be as ruleIsRequired,Z as ruleToSchema,O as schemaType};
1
+ export*from"./fetcher";export*from"./generate-client";export*from"./generate-openapi";export*from"./generate-types";export*from"./resource";export*from"./operation";
@@ -0,0 +1 @@
1
+ export function defineApiOperation(options){const{authorize,handle}=options;return Object.freeze({async execute(context){await authorize(context);return handle(context)}})}
@@ -0,0 +1 @@
1
+ export class MissingValue{static instance=new MissingValue;isMissing(){return!0}}export class MergeValue{data;constructor(data){this.data=data}}export class ConditionalValue{condition;value;defaultValue;constructor(condition,value,defaultValue=new MissingValue){this.condition=condition;this.value=value;this.defaultValue=defaultValue}resolve(){return(typeof this.condition==="function"?this.condition():this.condition)?this.value:this.defaultValue}}export class JsonResource{resource;additional={};request;static wrap="data";static additionalMeta={};constructor(resource){this.resource=resource}withRequest(request){this.request=request;return this}withAdditional(data){this.additional={...this.additional,...data};return this}resolve(request){const data=this.toArray(request||this.request);return this.filterAndResolve(data)}toResponse(request){const data=this.resolve(request),wrap=this.constructor.wrap;if(wrap)return{[wrap]:data,...this.additional};return{...data,...this.additional}}toJson(request){return JSON.stringify(this.toResponse(request))}toJSON(){return this.toResponse()}static collection(resources){return new ResourceCollection(resources,this)}when(condition,value,defaultValue){if(typeof condition==="function"?condition():condition)return typeof value==="function"?value():value;return defaultValue!==void 0?defaultValue:MissingValue.instance}whenNotNull(value,transform){if(value===null||value===void 0)return MissingValue.instance;return transform?transform(value):value}whenLoaded(relationship,value,defaultValue){const resource=this.resource;if(!((relationship in resource)&&resource[relationship]!==void 0))return defaultValue!==void 0?defaultValue:MissingValue.instance;if(value!==void 0)return typeof value==="function"?value():value;return resource[relationship]}whenCounted(relationship,defaultValue){const resource=this.resource,countKey=`${relationship}_count`;if(countKey in resource)return Number(resource[countKey]);return defaultValue!==void 0?defaultValue:MissingValue.instance}merge(data){return new MergeValue(data)}mergeWhen(condition,data){if(!(typeof condition==="function"?condition():condition))return MissingValue.instance;const resolvedData=typeof data==="function"?data():data;return new MergeValue(resolvedData)}static MAX_RESOLVE_DEPTH=20;filterAndResolve(data,depth=0){if(depth>=JsonResource.MAX_RESOLVE_DEPTH)return data;const result={};for(const[key,value]of Object.entries(data)){if(value instanceof MissingValue)continue;if(value instanceof MergeValue){Object.assign(result,this.filterAndResolve(value.data,depth+1));continue}if(value instanceof ConditionalValue){const resolved=value.resolve();if(!(resolved instanceof MissingValue))result[key]=resolved;continue}if(value!==null&&typeof value==="object"&&!Array.isArray(value)&&!(value instanceof Date)){result[key]=this.filterAndResolve(value,depth+1);continue}if(Array.isArray(value)){result[key]=value.filter((item)=>!(item instanceof MissingValue)).map((item)=>{if(item instanceof MergeValue)return item.data;if(item!==null&&typeof item==="object"&&!(item instanceof Date))return this.filterAndResolve(item,depth+1);return item});continue}result[key]=value}return result}static withoutWrapping(){this.wrap=null}static wrapWith(key){this.wrap=key}}export class ResourceCollection{resources;ResourceClass;additional={};request;static wrap="data";constructor(resources,ResourceClass){this.resources=resources;this.ResourceClass=ResourceClass}withRequest(request){this.request=request;return this}withAdditional(data){this.additional={...this.additional,...data};return this}resolve(request){const req=request||this.request;return this.resources.map((resource)=>{const instance=new this.ResourceClass(resource);if(req)instance.withRequest(req);return instance.resolve(req)})}toResponse(request){const data=this.resolve(request),wrap=this.constructor.wrap;if(wrap)return{[wrap]:data,...this.additional};return{data,...this.additional}}toJson(request){return JSON.stringify(this.toResponse(request))}count(){return this.resources.length}isEmpty(){return this.resources.length===0}isNotEmpty(){return this.resources.length>0}}export class PaginatedResourceCollection extends ResourceCollection{meta;links;constructor(paginatedData,ResourceClass){super(paginatedData.data,ResourceClass);this.meta=paginatedData.meta;this.links=paginatedData.links}static fromPagination(data,ResourceClass,options){const{currentPage,perPage,total,baseUrl=""}=options,lastPage=Math.ceil(total/perPage),from=total>0?(currentPage-1)*perPage+1:null,to=total>0?Math.min(currentPage*perPage,total):null;return new PaginatedResourceCollection({data,meta:{current_page:currentPage,from,last_page:lastPage,per_page:perPage,to,total},links:{first:`${baseUrl}?page=1`,last:`${baseUrl}?page=${lastPage}`,prev:currentPage>1?`${baseUrl}?page=${currentPage-1}`:null,next:currentPage<lastPage?`${baseUrl}?page=${currentPage+1}`:null}},ResourceClass)}toResponse(request){return{data:this.resolve(request),meta:this.meta,links:this.links,...this.additional}}}export function resource(data,transform){const transformed=transform(data);return{toResponse:()=>({data:transformed}),toJson:()=>JSON.stringify({data:transformed})}}export function collection(data,transform){const transformed=data.map(transform);return{toResponse:()=>({data:transformed}),toJson:()=>JSON.stringify({data:transformed})}}
package/dist/types.js ADDED
File without changes
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.190",
5
+ "version": "0.75.192",
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.190",
65
- "@stacksjs/path": "0.75.190"
64
+ "@stacksjs/config": "0.75.192",
65
+ "@stacksjs/path": "0.75.192"
66
66
  },
67
67
  "devDependencies": {
68
- "@stacksjs/utils": "0.75.190",
68
+ "@stacksjs/utils": "0.75.192",
69
69
  "better-dx": "^0.2.24"
70
70
  },
71
71
  "peerDependencies": {
72
- "@stacksjs/router": "0.75.190"
72
+ "@stacksjs/router": "0.75.192"
73
73
  },
74
74
  "peerDependenciesMeta": {
75
75
  "@stacksjs/router": {