resora 0.2.17 → 1.0.0

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/index.mjs CHANGED
@@ -1,7 +1,1122 @@
1
- import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,rmSync as i,writeFileSync as a}from"fs";import o,{dirname as s,join as c}from"path";import{createRequire as l}from"module";import{pathToFileURL as u}from"url";import{Command as d}from"@h3ravel/musket";function f(e){return e}let ee,p,m=[`meta`,`links`],h={first:`first`,last:`last`,prev:`prev`,next:`next`},g=``,_=`page`,v={to:`to`,from:`from`,links:`links`,path:`path`,total:`total`,per_page:`per_page`,last_page:`last_page`,current_page:`current_page`},y={previous:`previous`,next:`next`};const te=e=>{ee=e},ne=()=>ee,re=e=>{p=e},ie=()=>p,ae=e=>{p={...p||{},rootKey:e}},oe=e=>{p={...p||{},wrap:e}},se=()=>p?.wrap,ce=()=>p?.rootKey,le=e=>{p={...p||{},factory:e}},ue=()=>p?.factory,de=e=>{m=e},fe=()=>m,b=e=>{h={...h,...e}},x=()=>h,S=e=>{g=e},pe=()=>g,me=e=>{_=e},C=()=>_,he=e=>{v=e},ge=()=>v,_e=e=>{y={...y,...e}},ve=()=>y;let ye;const w=e=>{ye=e},T=()=>ye,E=e=>{if(!e||typeof e!=`object`)return typeof e==`string`?e:void 0;let t=e;return t.req&&typeof t.req==`object`?D(t.req):D(t)},D=e=>{if(typeof e.originalUrl==`string`)return e.originalUrl;if(typeof e.url==`string`)try{let t=new URL(e.url);return t.pathname+t.search}catch{return e.url}},O=e=>{if(!e||typeof e!=`object`)return;let t=e;return t.res&&typeof t.res==`object`?t.res:e},k=e=>{w(E(e))},A=e=>{if(typeof e!=`object`||!e||Array.isArray(e)||e instanceof Date||e instanceof RegExp)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null},j=(e,t,n=`data`)=>!t||Object.keys(t).length===0?e:Array.isArray(e)?{[n]:e,...t}:A(e)?{...e,...t}:{[n]:e,...t},M=(e,t)=>{if(!t)return e;if(!e)return t;let n={...e};for(let[e,r]of Object.entries(t)){let t=n[e];A(t)&&A(r)?n[e]=M(t,r):n[e]=r}return n},N=e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.toObject==`function`&&typeof t.getRawAttributes==`function`},P=e=>!e||typeof e!=`object`?!1:typeof e.all==`function`,F=e=>{if(Array.isArray(e))return e.map(e=>F(e));if(N(e))return F(e.toObject());if(P(e)){let t=e.all();return Array.isArray(t)?t.map(e=>F(e)):F(t)}return A(e)?Object.entries(e).reduce((e,[t,n])=>(e[t]=F(n),e),{}):e},I=()=>{let e=fe();return Array.isArray(e)?{metaKey:e.includes(`meta`)?`meta`:void 0,linksKey:e.includes(`links`)?`links`:void 0,cursorKey:e.includes(`cursor`)?`cursor`:void 0}:{metaKey:e.meta,linksKey:e.links,cursorKey:e.cursor}},be=e=>{if(e===null)return;let t=Number(e);if(!(!Number.isInteger(t)||t<1))return t},xe=(e,t)=>{try{return new URL(e,`http://resora.local`).searchParams.get(t)}catch{let n=e.indexOf(`?`);return n<0?null:new URLSearchParams(e.slice(n+1)).get(t)}},Se=(e,t=C()||`page`)=>{let n=e===void 0?T():E(e);if(n)return be(xe(n,t))},Ce=e=>t=>Se(e,t),L=(e,t)=>{if(e===void 0)return;let n=t||T()||``,r=pe()||``,i=C()||`page`,a=n.indexOf(`?`),o=a>=0?n.slice(0,a):n,s=a>=0?n.slice(a+1):``;if(/^https?:\/\//i.test(o)){let t=new URL(n);return t.searchParams.set(i,String(e)),t.toString()}let c=r.replace(/\/$/,``),l=o.replace(/^\//,``);if(/^https?:\/\//i.test(c)){let t=l?`${c}/${l}`:c,n=new URL(t);if(s)for(let[e,t]of new URLSearchParams(s))n.searchParams.set(e,t);return n.searchParams.set(i,String(e)),n.toString()}let u=[c,l].filter(Boolean).join(`/`),d=u?`/${u}`:`/`,f=new URLSearchParams(s);return f.set(i,String(e)),`${d}?${f.toString()}`},we=e=>{let t=e.currentPage,n=e.lastPage,r=e.hasMorePages;return{firstPage:typeof n==`number`?1:void 0,lastPage:n,nextPage:typeof n==`number`?typeof t==`number`&&t<n?t+1:void 0:r&&typeof t==`number`?t+1:void 0,prevPage:typeof t==`number`&&t>1?t-1:void 0}},R=(e,t)=>e.links&&Object.prototype.hasOwnProperty.call(e.links,t),z=e=>{let{metaKey:t,linksKey:n,cursorKey:r}=I(),i={},a=!!e&&typeof e==`object`&&!!e.meta&&typeof e.meta==`object`&&(Array.isArray(e.data)||P(e.data)),o=a?we(e.meta):void 0,s=a?{first:typeof e.firstPageUrl==`function`?e.firstPageUrl():void 0,last:typeof e.lastPageUrl==`function`?e.lastPageUrl():void 0,prev:typeof e.previousPageUrl==`function`?e.previousPageUrl():void 0,next:typeof e.nextPageUrl==`function`?e.nextPageUrl():void 0}:void 0,c=e?.pagination||(a?{...e.meta,...o||{},path:e.urlDriver?.path}:void 0),l=e?.cursor,u={},d={};if(c){let e=T()||c.path,t=e?L(c.currentPage,e)??e:void 0,n={first:R(c,`first`)?c.links.first:L(c.firstPage,e)??s?.first,last:R(c,`last`)?c.links.last:L(c.lastPage,e)??s?.last,prev:R(c,`prev`)?c.links.prev:L(c.prevPage,e)??s?.prev,next:R(c,`next`)?c.links.next:L(c.nextPage,e)??s?.next},r=Object.fromEntries(Object.entries(n).filter(([,e])=>e!==void 0)),i={to:c.to,from:c.from,links:c.links||(a&&Object.keys(r).length?r:void 0),path:t,total:c.total,per_page:c.perPage,last_page:c.lastPage,current_page:c.currentPage};for(let[e,t]of Object.entries(ge())){if(!t)continue;let n=i[e];n!==void 0&&(u[t]=n)}for(let[e,t]of Object.entries(x())){if(!t)continue;let r=n[e];r!==void 0&&(d[t]=r)}}if(l){let e={},t={previous:l.previous,next:l.next};for(let[n,r]of Object.entries(ve())){if(!r)continue;let i=t[n];i!==void 0&&(e[r]=i)}r&&Object.keys(e).length>0?i[r]=e:Object.keys(e).length>0&&(u.cursor=e)}return t&&Object.keys(u).length>0&&(i[t]=u),n&&Object.keys(d).length>0&&(i[n]=d),i},B=({payload:e,meta:t,metaKey:n=`meta`,wrap:r=!0,rootKey:i=`data`,factory:a,context:o})=>{if(a)return a(e,{...o,rootKey:i,meta:t});if(!r)return t===void 0?e:A(e)?{...e,[n]:t}:{[i]:e,[n]:t};let s={[i]:e};return t!==void 0&&(s[n]=t),s},Te=(e,t)=>{let n=e?.with;if(typeof n!=`function`||n===t||n.length>0)return;let r=n.call(e);return A(r)?r:void 0},V=Symbol(`resora.conditional.missing`),H=(e,t)=>e?typeof t==`function`?t():t:V,Ee=e=>e??V,De=(e,t)=>{if(!e)return{};let n=typeof t==`function`?t():t;return A(n)?n:{}},U=e=>{if(e===V)return V;if(Array.isArray(e))return e.map(e=>U(e)).filter(e=>e!==V);if(A(e)){let t={};for(let[n,r]of Object.entries(e)){let e=U(r);e!==V&&(t[n]=e)}return t}return e},W=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1 $2`).replace(/([A-Z]+)([A-Z][a-z])/g,`$1 $2`).replace(/[-_\s]+/g,` `).trim().toLowerCase().split(` `).filter(Boolean),Oe=e=>W(e).map((e,t)=>t===0?e:e.charAt(0).toUpperCase()+e.slice(1)).join(``),ke=e=>W(e).join(`_`),Ae=e=>W(e).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``),je=e=>W(e).join(`-`),G=e=>{if(typeof e==`function`)return e;switch(e){case`camel`:return Oe;case`snake`:return ke;case`pascal`:return Ae;case`kebab`:return je}},K=(e,t)=>e==null?e:Array.isArray(e)?e.map(e=>K(e,t)):e instanceof Date||e instanceof RegExp?e:typeof e==`object`?Object.fromEntries(Object.entries(e).map(([e,n])=>[t(e),K(n,t)])):e;let q=o.resolve(process.cwd(),`node_modules/resora/stubs`);t(q)||(q=o.resolve(process.cwd(),`stubs`));const Me=()=>({stubsDir:q,preferredCase:`camel`,responseStructure:{wrap:!0,rootKey:`data`},paginatedExtras:[`meta`,`links`],baseUrl:``,pageName:`page`,paginatedLinks:{first:`first`,last:`last`,prev:`prev`,next:`next`},paginatedMeta:{to:`to`,from:`from`,links:`links`,path:`path`,total:`total`,per_page:`per_page`,last_page:`last_page`,current_page:`current_page`},cursorMeta:{previous:`previous`,next:`next`},resourcesDir:`src/resources`,stubs:{config:`resora.config.stub`,resource:`resource.stub`,collection:`resource.collection.stub`}}),J=e=>{let t=Me();return Object.assign(t,e,{stubs:Object.assign(t.stubs,e.stubs||{})},{cursorMeta:Object.assign(t.cursorMeta,e.cursorMeta||{})},{paginatedMeta:e.paginatedMeta||t.paginatedMeta},{paginatedLinks:Object.assign(t.paginatedLinks,e.paginatedLinks||{})},{responseStructure:Object.assign(t.responseStructure,e.responseStructure||{})})};let Y=!1,X;const Ne=()=>{Y=!1,X=void 0},Pe=e=>{e.preferredCase!==`camel`&&te(e.preferredCase),re(e.responseStructure),de(e.paginatedExtras),b(e.paginatedLinks),he(e.paginatedMeta),_e(e.cursorMeta),S(e.baseUrl),me(e.pageName)},Fe=async e=>await import(`${u(e).href}?resora_runtime=${Date.now()}`),Ie=e=>{Pe(J((e?.default??e)||{})),Y=!0},Le=()=>{let e=l(import.meta.url),n=[o.join(process.cwd(),`resora.config.cjs`)];for(let r of n)if(t(r))try{return Ie(e(r)),!0}catch{continue}return!1},Z=async()=>{if(!Y){if(X)return await X;Le()||(X=(async()=>{let e=[o.join(process.cwd(),`resora.config.js`),o.join(process.cwd(),`resora.config.ts`)];for(let n of e)if(t(n))try{Ie(await Fe(n));return}catch{continue}Y=!0})(),await X)}};Z();var Re=class{command;config={};constructor(e={}){this.config=J(e)}async loadConfig(e={}){this.config=J(e);let n=[c(process.cwd(),`resora.config.ts`),c(process.cwd(),`resora.config.js`),c(process.cwd(),`resora.config.cjs`)];for(let e of n)if(t(e))try{let{default:t}=await import(e);Object.assign(this.config,t);break}catch(t){console.error(`Error loading config file at ${e}:`,t)}return this}getConfig(){return this.config}init(){let n=c(process.cwd(),`resora.config.js`),i=c(this.config.stubsDir,this.config.stubs.config);return t(n)&&!this.command.option(`force`)&&(this.command.error(`Error: ${n} already exists.`),process.exit(1)),this.ensureDirectory(n),t(n)&&this.command.option(`force`)&&e(n,n.replace(/\.js$/,`.backup.${Date.now()}.js`)),a(n,r(i,`utf-8`)),{path:n}}ensureDirectory(e){let r=s(e);t(r)||n(r,{recursive:!0})}generateFile(e,n,o,s){t(n)&&!s?.force?(this.command.error(`Error: ${n} already exists.`),process.exit(1)):t(n)&&s?.force&&i(n);let c=r(e,`utf-8`);for(let[e,t]of Object.entries(o))c=c.replace(RegExp(`{{${e}}}`,`g`),t);return this.ensureDirectory(n),a(n,c),n}makeResource(e,n){let r=e;n?.collection&&!e.endsWith(`Collection`)&&!e.endsWith(`Resource`)?r+=`Collection`:!n?.collection&&!e.endsWith(`Resource`)&&!e.endsWith(`Collection`)&&(r+=`Resource`);let i=`${r}.ts`,a=c(this.config.resourcesDir,i),o=c(this.config.stubsDir,n?.collection||e.endsWith(`Collection`)?this.config.stubs.collection:this.config.stubs.resource);t(o)||(this.command.error(`Error: Stub file ${o} not found.`),process.exit(1)),r=r.split(`/`).pop()?.split(`.`).shift();let s=r.replace(/(Resource|Collection)$/,``)+`Resource`,l=[`/**`,` * The resource that this collection collects.`,` */`,`collects = ${s}`].join(`
2
- `),u=`import ${s} from './${s}'\n`,d=(!!n?.collection||e.endsWith(`Collection`))&&t(c(this.config.resourcesDir,`${s}.ts`)),f=this.generateFile(o,a,{ResourceName:r,CollectionResourceName:r.replace(/(Resource|Collection)$/,``)+`Resource`,"collects = Resource":d?l:``,"import = Resource":d?u:``},n);return{name:r,path:f}}},ze=class extends d{signature=`init
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
+ import path, { dirname, join } from "path";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { createRequire } from "module";
5
+ import { pathToFileURL } from "url";
6
+ import { Command } from "@h3ravel/musket";
7
+
8
+ //#region src/ApiResource.ts
9
+ /**
10
+ * ApiResource function to return the Resource instance
11
+ *
12
+ * @param instance Resource instance
13
+ * @returns Resource instance
14
+ */
15
+ function ApiResource(instance) {
16
+ return instance;
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/utilities/state.ts
21
+ let globalPreferredCase;
22
+ let globalResponseStructure;
23
+ let globalPaginatedExtras = ["meta", "links"];
24
+ let globalPaginatedLinks = {
25
+ first: "first",
26
+ last: "last",
27
+ prev: "prev",
28
+ next: "next"
29
+ };
30
+ let globalBaseUrl = "";
31
+ let globalPageName = "page";
32
+ let globalPaginatedMeta = {
33
+ to: "to",
34
+ from: "from",
35
+ links: "links",
36
+ path: "path",
37
+ total: "total",
38
+ per_page: "per_page",
39
+ last_page: "last_page",
40
+ current_page: "current_page"
41
+ };
42
+ let globalCursorMeta = {
43
+ previous: "previous",
44
+ next: "next"
45
+ };
46
+ /**
47
+ * Sets the global case style for response keys, which will be applied
48
+ * to all responses unless overridden by individual resource configurations.
49
+ *
50
+ * @param style The case style to set as the global default for response keys.
51
+ */
52
+ const setGlobalCase = (style) => {
53
+ globalPreferredCase = style;
54
+ };
55
+ /**
56
+ * Retrieves the global case style for response keys, which is used
57
+ * to determine how keys in responses should be formatted.
58
+ *
59
+ * @returns
60
+ */
61
+ const getGlobalCase = () => {
62
+ return globalPreferredCase;
63
+ };
64
+ /**
65
+ * Sets the global response structure configuration, which defines how
66
+ * responses should be structured across the application.
67
+ *
68
+ * @param config The response structure configuration object.
69
+ */
70
+ const setGlobalResponseStructure = (config) => {
71
+ globalResponseStructure = config;
72
+ };
73
+ /**
74
+ * Retrieves the global response structure configuration, which
75
+ * defines how responses should be structured across the application.
76
+ *
77
+ * @returns
78
+ */
79
+ const getGlobalResponseStructure = () => {
80
+ return globalResponseStructure;
81
+ };
82
+ /**
83
+ * Sets the global response root key, which is the key under which
84
+ * the main data will be nested in responses if wrapping is enabled.
85
+ *
86
+ * @param rootKey The root key to set for response data.
87
+ */
88
+ const setGlobalResponseRootKey = (rootKey) => {
89
+ globalResponseStructure = {
90
+ ...globalResponseStructure || {},
91
+ rootKey
92
+ };
93
+ };
94
+ /**
95
+ * Sets the global response wrap option, which determines whether responses
96
+ * should be wrapped in a root key or returned unwrapped when possible.
97
+ *
98
+ * @param wrap The wrap option to set for responses.
99
+ */
100
+ const setGlobalResponseWrap = (wrap) => {
101
+ globalResponseStructure = {
102
+ ...globalResponseStructure || {},
103
+ wrap
104
+ };
105
+ };
106
+ /**
107
+ * Retrieves the global response wrap option, which indicates whether responses
108
+ * should be wrapped in a root key or returned unwrapped when possible.
109
+ *
110
+ * @returns
111
+ */
112
+ const getGlobalResponseWrap = () => {
113
+ return globalResponseStructure?.wrap;
114
+ };
115
+ /**
116
+ * Retrieves the global response root key, which is the key under which the main
117
+ * data will be nested in responses if wrapping is enabled.
118
+ *
119
+ * @returns
120
+ */
121
+ const getGlobalResponseRootKey = () => {
122
+ return globalResponseStructure?.rootKey;
123
+ };
124
+ /**
125
+ * Sets the global response factory, which is a custom function that can be used
126
+ * to produce a completely custom response structure based on the provided
127
+ * payload and context.
128
+ *
129
+ * @param factory The response factory function to set as the global default for response construction.
130
+ */
131
+ const setGlobalResponseFactory = (factory) => {
132
+ globalResponseStructure = {
133
+ ...globalResponseStructure || {},
134
+ factory
135
+ };
136
+ };
137
+ /**
138
+ * Retrieves the global response factory, which is a custom function that
139
+ * can be used to produce a completely custom response structure based on
140
+ * the provided payload and context.
141
+ *
142
+ * @returns
143
+ */
144
+ const getGlobalResponseFactory = () => {
145
+ return globalResponseStructure?.factory;
146
+ };
147
+ /**
148
+ * Sets the global paginated extras configuration, which defines the keys
149
+ * to use for pagination metadata, links, and cursor information in paginated responses.
150
+ *
151
+ * @param extras The paginated extras configuration object.
152
+ */
153
+ const setGlobalPaginatedExtras = (extras) => {
154
+ globalPaginatedExtras = extras;
155
+ };
156
+ /**
157
+ * Retrieves the global paginated extras configuration, which defines the keys to use for pagination metadata, links, and cursor information in paginated responses.
158
+ *
159
+ * @returns
160
+ */
161
+ const getGlobalPaginatedExtras = () => {
162
+ return globalPaginatedExtras;
163
+ };
164
+ /**
165
+ * Sets the global paginated links configuration, which defines the keys to
166
+ * use for pagination links (first, last, prev, next) in paginated responses.
167
+ *
168
+ * @param links The paginated links configuration object.
169
+ */
170
+ const setGlobalPaginatedLinks = (links) => {
171
+ globalPaginatedLinks = {
172
+ ...globalPaginatedLinks,
173
+ ...links
174
+ };
175
+ };
176
+ /**
177
+ * Retrieves the global paginated links configuration, which defines the keys to use for pagination links (first, last, prev, next) in paginated responses.
178
+ *
179
+ * @returns
180
+ */
181
+ const getGlobalPaginatedLinks = () => {
182
+ return globalPaginatedLinks;
183
+ };
184
+ /**
185
+ * Sets the global base URL, which is used for generating pagination links in responses.
186
+ *
187
+ * @param baseUrl The base URL to set for pagination link generation.
188
+ */
189
+ const setGlobalBaseUrl = (baseUrl) => {
190
+ globalBaseUrl = baseUrl;
191
+ };
192
+ /**
193
+ * Retrieves the global base URL, which is used for generating pagination links in responses.
194
+ *
195
+ * @returns
196
+ */
197
+ const getGlobalBaseUrl = () => {
198
+ return globalBaseUrl;
199
+ };
200
+ /**
201
+ * Sets the global page name, which is the query parameter name used for the page number in paginated requests and link generation.
202
+ *
203
+ * @param pageName
204
+ */
205
+ const setGlobalPageName = (pageName) => {
206
+ globalPageName = pageName;
207
+ };
208
+ /**
209
+ * Retrieves the global page name, which is the query parameter name
210
+ * used for the page number in paginated requests and link generation.
211
+ *
212
+ * @returns
213
+ */
214
+ const getGlobalPageName = () => {
215
+ return globalPageName;
216
+ };
217
+ /**
218
+ * Retrieves the keys to use for pagination extras (meta, links, cursor) based
219
+ * on the global configuration.
220
+ *
221
+ * @param meta Whether to include pagination metadata in the response.
222
+ */
223
+ const setGlobalPaginatedMeta = (meta) => {
224
+ globalPaginatedMeta = meta;
225
+ };
226
+ /**
227
+ * Retrieves the keys to use for pagination extras (meta, links, cursor) based
228
+ * on the global configuration.
229
+ *
230
+ * @returns The global pagination metadata configuration.
231
+ */
232
+ const getGlobalPaginatedMeta = () => {
233
+ return globalPaginatedMeta;
234
+ };
235
+ /**
236
+ * Sets the global cursor meta configuration, which defines the keys to use
237
+ * for cursor pagination metadata (previous, next) in responses.
238
+ *
239
+ * @param meta The cursor meta configuration object.
240
+ */
241
+ const setGlobalCursorMeta = (meta) => {
242
+ globalCursorMeta = {
243
+ ...globalCursorMeta,
244
+ ...meta
245
+ };
246
+ };
247
+ /**
248
+ * Retrieves the keys to use for cursor pagination metadata (previous, next) in
249
+ * responses based on the global configuration.
250
+ *
251
+ * @returns The global cursor pagination metadata configuration.
252
+ */
253
+ const getGlobalCursorMeta = () => {
254
+ return globalCursorMeta;
255
+ };
256
+ let globalRequestUrl;
257
+ let globalCtx;
258
+ const requestContextStore = new AsyncLocalStorage();
259
+ /**
260
+ * Sets the current request URL, used as a fallback for pagination link generation
261
+ * when no explicit path is provided in the pagination data.
262
+ *
263
+ * @param url The request URL or pathname to set.
264
+ */
265
+ const setRequestUrl = (url) => {
266
+ globalRequestUrl = url;
267
+ };
268
+ /**
269
+ * Retrieves the current request URL, used as a fallback path for pagination links.
270
+ *
271
+ * @returns The current request URL, or undefined if not set.
272
+ */
273
+ const getRequestUrl = () => {
274
+ return requestContextStore.getStore()?.url ?? globalRequestUrl;
275
+ };
276
+ const runWithCtx = (ctx, callback) => {
277
+ return requestContextStore.run({
278
+ ctx,
279
+ url: extractRequestUrl(ctx)
280
+ }, callback);
281
+ };
282
+ const getCtx = () => {
283
+ return requestContextStore.getStore()?.ctx ?? globalCtx;
284
+ };
285
+ /**
286
+ * Extracts the request URL pathname (with query string) from an HTTP context.
287
+ *
288
+ * Both Express middleware and H3 HTTPEvent expose `{ req, res }`. The `req`
289
+ * object carries URL information:
290
+ * - H3 / Web standard `Request`: `req.url` is a full URL string
291
+ * - Express `Request`: `req.originalUrl` is the pathname + query string
292
+ *
293
+ * The function also accepts a plain `req` object directly or a string URL.
294
+ *
295
+ * @param ctx An HTTP context `{ req, res }`, a bare `Request` object, or a string URL.
296
+ * @returns The pathname with query string, or undefined.
297
+ */
298
+ const extractRequestUrl = (ctx) => {
299
+ if (!ctx || typeof ctx !== "object") return typeof ctx === "string" ? ctx : void 0;
300
+ const obj = ctx;
301
+ if (obj.req && typeof obj.req === "object") return extractUrlFromRequest(obj.req);
302
+ return extractUrlFromRequest(obj);
303
+ };
304
+ /**
305
+ * Extracts the URL pathname + query string from a request object.
306
+ *
307
+ * Handles both Web standard `Request` (H3) where `url` is a full URL string,
308
+ * and Express `Request` where `originalUrl` or `url` provide the path.
309
+ */
310
+ const extractUrlFromRequest = (req) => {
311
+ if (typeof req.originalUrl === "string") return req.originalUrl;
312
+ if (typeof req.url === "string") try {
313
+ const parsed = new URL(req.url);
314
+ return parsed.pathname + parsed.search;
315
+ } catch {
316
+ return req.url;
317
+ }
318
+ };
319
+ /**
320
+ * Extracts the response object from an HTTP context.
321
+ *
322
+ * If the context has a `res` property (Express middleware `{ req, res }` or
323
+ * H3 HTTPEvent), returns `ctx.res`. Otherwise assumes the value is already
324
+ * a bare response object and returns it directly.
325
+ *
326
+ * @param ctx The HTTP context or bare response.
327
+ * @returns The response object, or undefined.
328
+ */
329
+ const extractResponseFromCtx = (ctx) => {
330
+ if (!ctx || typeof ctx !== "object") return;
331
+ const obj = ctx;
332
+ if (obj.res && typeof obj.res === "object") return obj.res;
333
+ return ctx;
334
+ };
335
+ /**
336
+ * Sets the current request context. Extracts the request URL from the provided
337
+ * context and stores it for use in pagination link generation.
338
+ *
339
+ * Can be called from middleware to make the request URL available to all
340
+ * resources created during the request lifecycle.
341
+ *
342
+ * @param ctx An HTTP context `{ req, res }`, Express Request, H3 HTTPEvent, or bare request.
343
+ */
344
+ const setCtx = (ctx) => {
345
+ globalCtx = ctx;
346
+ setRequestUrl(extractRequestUrl(ctx));
347
+ };
348
+
349
+ //#endregion
350
+ //#region src/utilities/objects.ts
351
+ /**
352
+ * Utility functions for working with objects, including type checking, merging, and property manipulation.
353
+ *
354
+ * @param value The value to check.
355
+ * @returns `true` if the value is a plain object, `false` otherwise.
356
+ */
357
+ const isPlainObject = (value) => {
358
+ if (typeof value !== "object" || value === null) return false;
359
+ if (Array.isArray(value) || value instanceof Date || value instanceof RegExp) return false;
360
+ const proto = Object.getPrototypeOf(value);
361
+ return proto === Object.prototype || proto === null;
362
+ };
363
+ /**
364
+ * Appends extra properties to a response body, ensuring that the main data is wrapped under a specified root key if necessary.
365
+ *
366
+ * @param body The original response body, which can be an object, array, or primitive value.
367
+ * @param extra Extra properties to append to the response body.
368
+ * @param rootKey The root key under which to wrap the main data if necessary.
369
+ * @returns The response body with the extra properties appended.
370
+ */
371
+ const appendRootProperties = (body, extra, rootKey = "data") => {
372
+ if (!extra || Object.keys(extra).length === 0) return body;
373
+ if (Array.isArray(body)) return {
374
+ [rootKey]: body,
375
+ ...extra
376
+ };
377
+ if (isPlainObject(body)) return {
378
+ ...body,
379
+ ...extra
380
+ };
381
+ return {
382
+ [rootKey]: body,
383
+ ...extra
384
+ };
385
+ };
386
+ /**
387
+ * Deeply merges two metadata objects, combining nested objects recursively.
388
+ *
389
+ * @param base The base metadata object to merge into.
390
+ * @param incoming The incoming metadata object to merge from.
391
+ * @returns
392
+ */
393
+ const mergeMetadata = (base, incoming) => {
394
+ if (!incoming) return base;
395
+ if (!base) return incoming;
396
+ const merged = { ...base };
397
+ for (const [key, value] of Object.entries(incoming)) {
398
+ const existing = merged[key];
399
+ if (isPlainObject(existing) && isPlainObject(value)) merged[key] = mergeMetadata(existing, value);
400
+ else merged[key] = value;
401
+ }
402
+ return merged;
403
+ };
404
+
405
+ //#endregion
406
+ //#region src/utilities/arkorm.ts
407
+ /**
408
+ * Type guard to check if a value is an Arkorm-like model, which is defined as an object
409
+ * that has a toObject method and optionally getRawAttributes, getAttribute, and
410
+ * setAttribute methods.
411
+ *
412
+ * @param value The value to check
413
+ * @returns True if the value is an Arkorm-like model, false otherwise
414
+ */
415
+ const isArkormLikeModel = (value) => {
416
+ if (!value || typeof value !== "object") return false;
417
+ const candidate = value;
418
+ return typeof candidate.toObject === "function" && typeof candidate.getRawAttributes === "function";
419
+ };
420
+ /**
421
+ * Type guard to check if a value is an Arkorm-like collection, which is defined as an object
422
+ *
423
+ * @param value
424
+ * @returns
425
+ */
426
+ const isArkormLikeCollection = (value) => {
427
+ if (!value || typeof value !== "object") return false;
428
+ return typeof value.all === "function";
429
+ };
430
+ /**
431
+ * Normalize a value for serialization by recursively converting Arkorm-like models and
432
+ * collections to plain objects, while preserving the structure of arrays and plain objects.
433
+ *
434
+ * @param value The value to normalize
435
+ * @returns The normalized value, ready for serialization
436
+ */
437
+ const normalizeSerializableData = (value) => {
438
+ if (Array.isArray(value)) return value.map((item) => normalizeSerializableData(item));
439
+ if (isArkormLikeModel(value)) return normalizeSerializableData(value.toObject());
440
+ if (isArkormLikeCollection(value)) {
441
+ const collectionData = value.all();
442
+ if (Array.isArray(collectionData)) return collectionData.map((item) => normalizeSerializableData(item));
443
+ return normalizeSerializableData(collectionData);
444
+ }
445
+ if (isPlainObject(value)) return Object.entries(value).reduce((accumulator, [key, nestedValue]) => {
446
+ accumulator[key] = normalizeSerializableData(nestedValue);
447
+ return accumulator;
448
+ }, {});
449
+ return value;
450
+ };
451
+
452
+ //#endregion
453
+ //#region src/utilities/pagination.ts
454
+ /**
455
+ * Retrieves the configured keys for pagination extras (meta, links, cursor) based on the application's configuration.
456
+ *
457
+ * @returns An object containing the keys for meta, links, and cursor extras, or `undefined` if not configured.
458
+ */
459
+ const getPaginationExtraKeys = () => {
460
+ const extras = getGlobalPaginatedExtras();
461
+ if (Array.isArray(extras)) return {
462
+ metaKey: extras.includes("meta") ? "meta" : void 0,
463
+ linksKey: extras.includes("links") ? "links" : void 0,
464
+ cursorKey: extras.includes("cursor") ? "cursor" : void 0
465
+ };
466
+ return {
467
+ metaKey: extras.meta,
468
+ linksKey: extras.links,
469
+ cursorKey: extras.cursor
470
+ };
471
+ };
472
+ const parsePageValue = (value) => {
473
+ if (value === null) return;
474
+ const page = Number(value);
475
+ if (!Number.isInteger(page) || page < 1) return;
476
+ return page;
477
+ };
478
+ const getPageValueFromUrl = (requestUrl, pageName) => {
479
+ try {
480
+ return new URL(requestUrl, "http://resora.local").searchParams.get(pageName);
481
+ } catch {
482
+ const qIndex = requestUrl.indexOf("?");
483
+ if (qIndex < 0) return null;
484
+ return new URLSearchParams(requestUrl.slice(qIndex + 1)).get(pageName);
485
+ }
486
+ };
487
+ /**
488
+ * Resolves the current page number from a request context or the globally
489
+ * stored request URL, using the configured page query name by default.
490
+ *
491
+ * @param ctx Optional request context or URL.
492
+ * @param pageName Optional page query parameter name.
493
+ * @returns
494
+ */
495
+ const resolveCurrentPage = (ctx, pageName = getGlobalPageName() || "page") => {
496
+ const requestUrl = typeof ctx === "undefined" ? getRequestUrl() : extractRequestUrl(ctx);
497
+ if (!requestUrl) return;
498
+ return parsePageValue(getPageValueFromUrl(requestUrl, pageName));
499
+ };
500
+ /**
501
+ * Creates an Arkorm-compatible current-page resolver backed by Resora's
502
+ * request-context URL parsing.
503
+ *
504
+ * @param ctx Optional request context or URL.
505
+ * @returns
506
+ */
507
+ const createArkormCurrentPageResolver = (ctx) => {
508
+ return (pageName) => resolveCurrentPage(ctx, pageName);
509
+ };
510
+ /**
511
+ * Builds a pagination URL for a given page number and path, using the global base URL and page name configuration.
512
+ *
513
+ * URL resolution follows a three-tier priority:
514
+ * 1. Full URL – when `pathName` is absolute or `baseUrl` is set alongside a path
515
+ * 2. Path-relative – when only a path is available (e.g. `/users?page=2`)
516
+ * 3. Bare fallback – `/?page=X` when neither base URL nor path is available
517
+ *
518
+ * @param page The page number for which to build the URL. If `undefined`, the function will return `undefined`.
519
+ * @param pathName The path to use for the URL. If not provided, it will default to an empty string.
520
+ * @returns
521
+ */
522
+ const buildPageUrl = (page, pathName) => {
523
+ if (typeof page === "undefined") return;
524
+ const rawPath = pathName || getRequestUrl() || "";
525
+ const base = getGlobalBaseUrl() || "";
526
+ const pageName = getGlobalPageName() || "page";
527
+ const qIndex = rawPath.indexOf("?");
528
+ const pathOnly = qIndex >= 0 ? rawPath.slice(0, qIndex) : rawPath;
529
+ const existingSearch = qIndex >= 0 ? rawPath.slice(qIndex + 1) : "";
530
+ if (/^https?:\/\//i.test(pathOnly)) {
531
+ const url = new URL(rawPath);
532
+ url.searchParams.set(pageName, String(page));
533
+ return url.toString();
534
+ }
535
+ const normalizedBase = base.replace(/\/$/, "");
536
+ const normalizedPath = pathOnly.replace(/^\//, "");
537
+ if (/^https?:\/\//i.test(normalizedBase)) {
538
+ const root = normalizedPath ? `${normalizedBase}/${normalizedPath}` : normalizedBase;
539
+ const url = new URL(root);
540
+ if (existingSearch) for (const [k, v] of new URLSearchParams(existingSearch)) url.searchParams.set(k, v);
541
+ url.searchParams.set(pageName, String(page));
542
+ return url.toString();
543
+ }
544
+ const segments = [normalizedBase, normalizedPath].filter(Boolean).join("/");
545
+ const pathBase = segments ? `/${segments}` : "/";
546
+ const params = new URLSearchParams(existingSearch);
547
+ params.set(pageName, String(page));
548
+ return `${pathBase}?${params.toString()}`;
549
+ };
550
+ /**
551
+ * Derives page numbers (firstPage, lastPage, nextPage, prevPage) from an
552
+ * Arkorm-style pagination meta object so that buildPageUrl() can generate
553
+ * links that respect the auto-detected request URL and query string.
554
+ */
555
+ const derivePageNumbers = (meta) => {
556
+ const currentPage = meta.currentPage;
557
+ const lastPage = meta.lastPage;
558
+ const hasMorePages = meta.hasMorePages;
559
+ return {
560
+ firstPage: typeof lastPage === "number" ? 1 : void 0,
561
+ lastPage,
562
+ nextPage: typeof lastPage === "number" ? typeof currentPage === "number" && currentPage < lastPage ? currentPage + 1 : void 0 : hasMorePages && typeof currentPage === "number" ? currentPage + 1 : void 0,
563
+ prevPage: typeof currentPage === "number" && currentPage > 1 ? currentPage - 1 : void 0
564
+ };
565
+ };
566
+ /**
567
+ * Extracts a pagination link (e.g. 'first', 'last', 'prev', 'next') from a
568
+ * pagination object, if it exists.
569
+ *
570
+ * @param pagination
571
+ * @param rel
572
+ * @returns
573
+ */
574
+ const hasPaginationLink = (pagination, rel) => {
575
+ return pagination.links && Object.prototype.hasOwnProperty.call(pagination.links, rel);
576
+ };
577
+ /**
578
+ * Builds pagination extras (meta, links, cursor) for a given resource based on
579
+ * its pagination and cursor properties, using the configured keys for each type of extra.
580
+ *
581
+ * @param resource The resource for which to build pagination extras.
582
+ * @returns An object containing the pagination extras (meta, links, cursor) for the resource.
583
+ */
584
+ const buildPaginationExtras = (resource) => {
585
+ const { metaKey, linksKey, cursorKey } = getPaginationExtraKeys();
586
+ const extra = {};
587
+ const isArkormPaginatorLike = !!resource && typeof resource === "object" && !!resource.meta && typeof resource.meta === "object" && (Array.isArray(resource.data) || isArkormLikeCollection(resource.data));
588
+ const arkormPageNumbers = isArkormPaginatorLike ? derivePageNumbers(resource.meta) : void 0;
589
+ const arkormFallbackLinks = isArkormPaginatorLike ? {
590
+ first: typeof resource.firstPageUrl === "function" ? resource.firstPageUrl() : void 0,
591
+ last: typeof resource.lastPageUrl === "function" ? resource.lastPageUrl() : void 0,
592
+ prev: typeof resource.previousPageUrl === "function" ? resource.previousPageUrl() : void 0,
593
+ next: typeof resource.nextPageUrl === "function" ? resource.nextPageUrl() : void 0
594
+ } : void 0;
595
+ const pagination = resource?.pagination || (isArkormPaginatorLike ? {
596
+ ...resource.meta,
597
+ ...arkormPageNumbers || {},
598
+ path: resource.urlDriver?.path
599
+ } : void 0);
600
+ const cursor = resource?.cursor;
601
+ const metaBlock = {};
602
+ const linksBlock = {};
603
+ if (pagination) {
604
+ const effectivePath = getRequestUrl() || pagination.path;
605
+ const currentPagePath = effectivePath ? buildPageUrl(pagination.currentPage, effectivePath) ?? effectivePath : void 0;
606
+ const linksSource = {
607
+ first: hasPaginationLink(pagination, "first") ? pagination.links.first : buildPageUrl(pagination.firstPage, effectivePath) ?? arkormFallbackLinks?.first,
608
+ last: hasPaginationLink(pagination, "last") ? pagination.links.last : buildPageUrl(pagination.lastPage, effectivePath) ?? arkormFallbackLinks?.last,
609
+ prev: hasPaginationLink(pagination, "prev") ? pagination.links.prev : buildPageUrl(pagination.prevPage, effectivePath) ?? arkormFallbackLinks?.prev,
610
+ next: hasPaginationLink(pagination, "next") ? pagination.links.next : buildPageUrl(pagination.nextPage, effectivePath) ?? arkormFallbackLinks?.next
611
+ };
612
+ const resolvedLinks = Object.fromEntries(Object.entries(linksSource).filter(([, v]) => v !== void 0));
613
+ const metaSource = {
614
+ to: pagination.to,
615
+ from: pagination.from,
616
+ links: pagination.links || (isArkormPaginatorLike && Object.keys(resolvedLinks).length ? resolvedLinks : void 0),
617
+ path: currentPagePath,
618
+ total: pagination.total,
619
+ per_page: pagination.perPage,
620
+ last_page: pagination.lastPage,
621
+ current_page: pagination.currentPage
622
+ };
623
+ for (const [sourceKey, outputKey] of Object.entries(getGlobalPaginatedMeta())) {
624
+ if (!outputKey) continue;
625
+ const value = metaSource[sourceKey];
626
+ if (typeof value !== "undefined") metaBlock[outputKey] = value;
627
+ }
628
+ for (const [sourceKey, outputKey] of Object.entries(getGlobalPaginatedLinks())) {
629
+ if (!outputKey) continue;
630
+ const value = linksSource[sourceKey];
631
+ if (typeof value !== "undefined") linksBlock[outputKey] = value;
632
+ }
633
+ }
634
+ if (cursor) {
635
+ const cursorBlock = {};
636
+ const cursorSource = {
637
+ previous: cursor.previous,
638
+ next: cursor.next
639
+ };
640
+ for (const [sourceKey, outputKey] of Object.entries(getGlobalCursorMeta())) {
641
+ if (!outputKey) continue;
642
+ const value = cursorSource[sourceKey];
643
+ if (typeof value !== "undefined") cursorBlock[outputKey] = value;
644
+ }
645
+ if (cursorKey && Object.keys(cursorBlock).length > 0) extra[cursorKey] = cursorBlock;
646
+ else if (Object.keys(cursorBlock).length > 0) metaBlock.cursor = cursorBlock;
647
+ }
648
+ if (metaKey && Object.keys(metaBlock).length > 0) extra[metaKey] = metaBlock;
649
+ if (linksKey && Object.keys(linksBlock).length > 0) extra[linksKey] = linksBlock;
650
+ return extra;
651
+ };
652
+
653
+ //#endregion
654
+ //#region src/utilities/response.ts
655
+ /**
656
+ * Builds a response envelope based on the provided payload,
657
+ * metadata, and configuration options.
658
+ *
659
+ * @param config The configuration object containing payload, metadata, and other options for building the response.
660
+ */
661
+ const buildResponseEnvelope = ({ payload, meta, metaKey = "meta", wrap = true, rootKey = "data", factory, context }) => {
662
+ if (factory) return factory(payload, {
663
+ ...context,
664
+ rootKey,
665
+ meta
666
+ });
667
+ if (!wrap) {
668
+ if (typeof meta === "undefined") return payload;
669
+ if (isPlainObject(payload)) return {
670
+ ...payload,
671
+ [metaKey]: meta
672
+ };
673
+ return {
674
+ [rootKey]: payload,
675
+ [metaKey]: meta
676
+ };
677
+ }
678
+ const body = { [rootKey]: payload };
679
+ if (typeof meta !== "undefined") body[metaKey] = meta;
680
+ return body;
681
+ };
682
+
683
+ //#endregion
684
+ //#region src/utilities/metadata.ts
685
+ /**
686
+ * Resolves metadata from a resource instance by checking for a custom `with` method.
687
+ * If the method exists and is different from the base method, it calls it to retrieve metadata.
688
+ * This allows resources to provide additional metadata for response construction.
689
+ *
690
+ * @param instance The resource instance to check for a custom `with` method.
691
+ * @param baseWithMethod The base `with` method to compare against.
692
+ * @returns The resolved metadata or `undefined` if no custom metadata is provided.
693
+ */
694
+ const resolveWithHookMetadata = (instance, baseWithMethod) => {
695
+ const candidate = instance?.with;
696
+ if (typeof candidate !== "function" || candidate === baseWithMethod) return;
697
+ if (candidate.length > 0) return;
698
+ const result = candidate.call(instance);
699
+ return isPlainObject(result) ? result : void 0;
700
+ };
701
+
702
+ //#endregion
703
+ //#region src/utilities/conditional.ts
704
+ const CONDITIONAL_ATTRIBUTE_MISSING = Symbol("resora.conditional.missing");
705
+ /**
706
+ * Resolves a value based on a condition. If the condition is falsy, it returns a special symbol indicating that the attribute is missing.
707
+ *
708
+ *
709
+ * @param condition The condition to evaluate.
710
+ * @param value The value or function to resolve if the condition is truthy.
711
+ * @returns The resolved value or a symbol indicating the attribute is missing.
712
+ */
713
+ const resolveWhen = (condition, value) => {
714
+ if (!condition) return CONDITIONAL_ATTRIBUTE_MISSING;
715
+ return typeof value === "function" ? value() : value;
716
+ };
717
+ /**
718
+ * Resolves a value only if it is not null or undefined.
719
+ *
720
+ * @param value The value to resolve.
721
+ * @returns The resolved value or a symbol indicating the attribute is missing.
722
+ */
723
+ const resolveWhenNotNull = (value) => {
724
+ return value === null || typeof value === "undefined" ? CONDITIONAL_ATTRIBUTE_MISSING : value;
725
+ };
726
+ /**
727
+ * Conditionally merges object attributes based on a condition.
728
+ *
729
+ * @param condition
730
+ * @param value
731
+ * @returns
732
+ */
733
+ const resolveMergeWhen = (condition, value) => {
734
+ if (!condition) return {};
735
+ const resolved = typeof value === "function" ? value() : value;
736
+ return isPlainObject(resolved) ? resolved : {};
737
+ };
738
+ /**
739
+ * Recursively sanitizes an object or array by removing any attributes that are marked as missing using the special symbol.
740
+ *
741
+ * @param value The value to sanitize.
742
+ * @returns The sanitized value.
743
+ */
744
+ const sanitizeConditionalAttributes = (value) => {
745
+ if (value === CONDITIONAL_ATTRIBUTE_MISSING) return CONDITIONAL_ATTRIBUTE_MISSING;
746
+ if (Array.isArray(value)) return value.map((item) => sanitizeConditionalAttributes(item)).filter((item) => item !== CONDITIONAL_ATTRIBUTE_MISSING);
747
+ if (isPlainObject(value)) {
748
+ const result = {};
749
+ for (const [key, nestedValue] of Object.entries(value)) {
750
+ const sanitizedValue = sanitizeConditionalAttributes(nestedValue);
751
+ if (sanitizedValue === CONDITIONAL_ATTRIBUTE_MISSING) continue;
752
+ result[key] = sanitizedValue;
753
+ }
754
+ return result;
755
+ }
756
+ return value;
757
+ };
758
+
759
+ //#endregion
760
+ //#region src/utilities/case.ts
761
+ /**
762
+ * Splits a string into words based on common delimiters and capitalization patterns.
763
+ * Handles camelCase, PascalCase, snake_case, kebab-case, and space-separated words.
764
+ *
765
+ * @param str
766
+ * @returns
767
+ */
768
+ const splitWords = (str) => {
769
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_\s]+/g, " ").trim().toLowerCase().split(" ").filter(Boolean);
770
+ };
771
+ /**
772
+ * Transforms a string to camelCase.
773
+ *
774
+ * @param str
775
+ * @returns
776
+ */
777
+ const toCamelCase = (str) => {
778
+ return splitWords(str).map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("");
779
+ };
780
+ /**
781
+ * Transforms a string to snake_case.
782
+ *
783
+ * @param str
784
+ * @returns
785
+ */
786
+ const toSnakeCase = (str) => {
787
+ return splitWords(str).join("_");
788
+ };
789
+ /**
790
+ * Transforms a string to PascalCase.
791
+ *
792
+ * @param str
793
+ * @returns
794
+ */
795
+ const toPascalCase = (str) => {
796
+ return splitWords(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
797
+ };
798
+ /**
799
+ * Transforms a string to kebab-case.
800
+ *
801
+ * @param str
802
+ * @returns
803
+ */
804
+ const toKebabCase = (str) => {
805
+ return splitWords(str).join("-");
806
+ };
807
+ /**
808
+ * Retrieves the appropriate case transformation function based on the provided style.
809
+ *
810
+ * @param style
811
+ * @returns
812
+ */
813
+ const getCaseTransformer = (style) => {
814
+ if (typeof style === "function") return style;
815
+ switch (style) {
816
+ case "camel": return toCamelCase;
817
+ case "snake": return toSnakeCase;
818
+ case "pascal": return toPascalCase;
819
+ case "kebab": return toKebabCase;
820
+ }
821
+ };
822
+ /**
823
+ * Transforms the keys of an object (including nested objects and arrays) using
824
+ * the provided transformer function.
825
+ *
826
+ * @param obj
827
+ * @param transformer
828
+ * @returns
829
+ */
830
+ const transformKeys = (obj, transformer) => {
831
+ if (obj === null || obj === void 0) return obj;
832
+ if (Array.isArray(obj)) return obj.map((item) => transformKeys(item, transformer));
833
+ if (obj instanceof Date || obj instanceof RegExp) return obj;
834
+ if (typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([key, value]) => [transformer(key), transformKeys(value, transformer)]));
835
+ return obj;
836
+ };
837
+
838
+ //#endregion
839
+ //#region src/utilities/config.ts
840
+ let stubsDir = path.resolve(process.cwd(), "node_modules/resora/stubs");
841
+ if (!existsSync(stubsDir)) stubsDir = path.resolve(process.cwd(), "stubs");
842
+ /**
843
+ * Get the default configuration for the application
844
+ *
845
+ * @returns
846
+ */
847
+ const getDefaultConfig = () => {
848
+ return {
849
+ stubsDir,
850
+ preferredCase: "camel",
851
+ responseStructure: {
852
+ wrap: true,
853
+ rootKey: "data"
854
+ },
855
+ paginatedExtras: ["meta", "links"],
856
+ baseUrl: "",
857
+ pageName: "page",
858
+ paginatedLinks: {
859
+ first: "first",
860
+ last: "last",
861
+ prev: "prev",
862
+ next: "next"
863
+ },
864
+ paginatedMeta: {
865
+ to: "to",
866
+ from: "from",
867
+ links: "links",
868
+ path: "path",
869
+ total: "total",
870
+ per_page: "per_page",
871
+ last_page: "last_page",
872
+ current_page: "current_page"
873
+ },
874
+ cursorMeta: {
875
+ previous: "previous",
876
+ next: "next"
877
+ },
878
+ resourcesDir: "src/resources",
879
+ stubs: {
880
+ config: "resora.config.stub",
881
+ resource: "resource.stub",
882
+ collection: "resource.collection.stub"
883
+ }
884
+ };
885
+ };
886
+ /**
887
+ * Defines the configuration for the application by merging the provided configuration with the default configuration. This function takes a partial configuration object as input and returns a complete configuration object that includes all required properties, using default values for any properties that are not specified in the input.
888
+ *
889
+ * @param config
890
+ * @returns
891
+ */
892
+ const defineConfig = (config) => {
893
+ const defConf = getDefaultConfig();
894
+ return Object.assign(defConf, config, { stubs: Object.assign(defConf.stubs, config.stubs || {}) }, { cursorMeta: Object.assign(defConf.cursorMeta, config.cursorMeta || {}) }, { paginatedMeta: config.paginatedMeta || defConf.paginatedMeta }, { paginatedLinks: Object.assign(defConf.paginatedLinks, config.paginatedLinks || {}) }, { responseStructure: Object.assign(defConf.responseStructure, config.responseStructure || {}) });
895
+ };
896
+
897
+ //#endregion
898
+ //#region src/utilities/runtime-config.ts
899
+ let runtimeConfigLoaded = false;
900
+ let runtimeConfigLoadingPromise;
901
+ /**
902
+ * Resets the runtime configuration state for testing purposes.
903
+ *
904
+ * @returns
905
+ */
906
+ const resetRuntimeConfigForTests = () => {
907
+ runtimeConfigLoaded = false;
908
+ runtimeConfigLoadingPromise = void 0;
909
+ };
910
+ /**
911
+ * Applies the provided configuration to the global state of the application.
912
+ *
913
+ * @param config The complete configuration object to apply.
914
+ */
915
+ const applyRuntimeConfig = (config) => {
916
+ if (config.preferredCase !== "camel") setGlobalCase(config.preferredCase);
917
+ setGlobalResponseStructure(config.responseStructure);
918
+ setGlobalPaginatedExtras(config.paginatedExtras);
919
+ setGlobalPaginatedLinks(config.paginatedLinks);
920
+ setGlobalPaginatedMeta(config.paginatedMeta);
921
+ setGlobalCursorMeta(config.cursorMeta);
922
+ setGlobalBaseUrl(config.baseUrl);
923
+ setGlobalPageName(config.pageName);
924
+ };
925
+ /**
926
+ * Loads the runtime configuration by searching for configuration files in the current working directory.
927
+ * @param configPath The path to the configuration file to load.
928
+ * @returns
929
+ */
930
+ const importConfigFile = async (configPath) => {
931
+ return await import(`${pathToFileURL(configPath).href}?resora_runtime=${Date.now()}`);
932
+ };
933
+ /**
934
+ * Resolves the imported configuration and applies it to the global state.
935
+ *
936
+ * @param imported
937
+ */
938
+ const resolveAndApply = (imported) => {
939
+ applyRuntimeConfig(defineConfig((imported?.default ?? imported) || {}));
940
+ runtimeConfigLoaded = true;
941
+ };
942
+ /**
943
+ * Loads the runtime configuration synchronously by searching for CommonJS configuration files in the current working directory.
944
+ *
945
+ * @returns
946
+ */
947
+ const loadRuntimeConfigSync = () => {
948
+ const require = createRequire(import.meta.url);
949
+ const syncConfigPaths = [path.join(process.cwd(), "resora.config.cjs")];
950
+ for (const configPath of syncConfigPaths) {
951
+ if (!existsSync(configPath)) continue;
952
+ try {
953
+ resolveAndApply(require(configPath));
954
+ return true;
955
+ } catch {
956
+ continue;
957
+ }
958
+ }
959
+ return false;
960
+ };
961
+ /**
962
+ * Loads the runtime configuration by searching for configuration files in the current working directory.
963
+ *
964
+ * @returns
965
+ */
966
+ const loadRuntimeConfig = async () => {
967
+ if (runtimeConfigLoaded) return;
968
+ if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
969
+ if (loadRuntimeConfigSync()) return;
970
+ runtimeConfigLoadingPromise = (async () => {
971
+ const possibleConfigPaths = [path.join(process.cwd(), "resora.config.js"), path.join(process.cwd(), "resora.config.ts")];
972
+ for (const configPath of possibleConfigPaths) {
973
+ if (!existsSync(configPath)) continue;
974
+ try {
975
+ resolveAndApply(await importConfigFile(configPath));
976
+ return;
977
+ } catch {
978
+ continue;
979
+ }
980
+ }
981
+ runtimeConfigLoaded = true;
982
+ })();
983
+ await runtimeConfigLoadingPromise;
984
+ };
985
+ loadRuntimeConfig();
986
+
987
+ //#endregion
988
+ //#region src/cli/CliApp.ts
989
+ var CliApp = class {
990
+ command;
991
+ config = {};
992
+ constructor(config = {}) {
993
+ this.config = defineConfig(config);
994
+ }
995
+ async loadConfig(config = {}) {
996
+ this.config = defineConfig(config);
997
+ const possibleConfigPaths = [
998
+ join(process.cwd(), "resora.config.ts"),
999
+ join(process.cwd(), "resora.config.js"),
1000
+ join(process.cwd(), "resora.config.cjs")
1001
+ ];
1002
+ for (const configPath of possibleConfigPaths) if (existsSync(configPath)) try {
1003
+ const { default: userConfig } = await import(configPath);
1004
+ Object.assign(this.config, userConfig);
1005
+ break;
1006
+ } catch (e) {
1007
+ console.error(`Error loading config file at ${configPath}:`, e);
1008
+ }
1009
+ return this;
1010
+ }
1011
+ /**
1012
+ * Get the current configuration object
1013
+ * @returns
1014
+ */
1015
+ getConfig() {
1016
+ return this.config;
1017
+ }
1018
+ /**
1019
+ * Initialize Resora by creating a default config file in the current directory
1020
+ *
1021
+ * @returns
1022
+ */
1023
+ init() {
1024
+ const outputPath = join(process.cwd(), "resora.config.js");
1025
+ const stubPath = join(this.config.stubsDir, this.config.stubs.config);
1026
+ if (existsSync(outputPath) && !this.command.option("force")) {
1027
+ this.command.error(`Error: ${outputPath} already exists.`);
1028
+ process.exit(1);
1029
+ }
1030
+ this.ensureDirectory(outputPath);
1031
+ if (existsSync(outputPath) && this.command.option("force")) copyFileSync(outputPath, outputPath.replace(/\.js$/, `.backup.${Date.now()}.js`));
1032
+ writeFileSync(outputPath, readFileSync(stubPath, "utf-8"));
1033
+ return { path: outputPath };
1034
+ }
1035
+ /**
1036
+ * Utility to ensure directory exists
1037
+ *
1038
+ * @param filePath
1039
+ */
1040
+ ensureDirectory(filePath) {
1041
+ const dir = dirname(filePath);
1042
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1043
+ }
1044
+ /**
1045
+ * Utility to generate file from stub
1046
+ *
1047
+ * @param stubPath
1048
+ * @param outputPath
1049
+ * @param replacements
1050
+ */
1051
+ generateFile(stubPath, outputPath, replacements, options) {
1052
+ if (existsSync(outputPath) && !options?.force) {
1053
+ this.command.error(`Error: ${outputPath} already exists.`);
1054
+ process.exit(1);
1055
+ } else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
1056
+ let content = readFileSync(stubPath, "utf-8");
1057
+ for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
1058
+ this.ensureDirectory(outputPath);
1059
+ writeFileSync(outputPath, content);
1060
+ return outputPath;
1061
+ }
1062
+ /**
1063
+ * Command to create a new resource or resource collection file
1064
+ *
1065
+ * @param name
1066
+ * @param options
1067
+ */
1068
+ makeResource(name, options) {
1069
+ let resourceName = name;
1070
+ if (options?.collection && !name.endsWith("Collection") && !name.endsWith("Resource")) resourceName += "Collection";
1071
+ else if (!options?.collection && !name.endsWith("Resource") && !name.endsWith("Collection")) resourceName += "Resource";
1072
+ const fileName = `${resourceName}.ts`;
1073
+ const outputPath = join(this.config.resourcesDir, fileName);
1074
+ const stubPath = join(this.config.stubsDir, options?.collection || name.endsWith("Collection") ? this.config.stubs.collection : this.config.stubs.resource);
1075
+ if (!existsSync(stubPath)) {
1076
+ this.command.error(`Error: Stub file ${stubPath} not found.`);
1077
+ process.exit(1);
1078
+ }
1079
+ resourceName = resourceName.split("/").pop()?.split(".").shift();
1080
+ const collectsName = resourceName.replace(/(Resource|Collection)$/, "") + "Resource";
1081
+ const collects = [
1082
+ "/**",
1083
+ " * The resource that this collection collects.",
1084
+ " */",
1085
+ `collects = ${collectsName}`
1086
+ ].join("\n");
1087
+ const collectsImport = `import ${collectsName} from './${collectsName}'\n`;
1088
+ const hasCollects = (!!options?.collection || name.endsWith("Collection")) && existsSync(join(this.config.resourcesDir, `${collectsName}.ts`));
1089
+ const path = this.generateFile(stubPath, outputPath, {
1090
+ ResourceName: resourceName,
1091
+ CollectionResourceName: resourceName.replace(/(Resource|Collection)$/, "") + "Resource",
1092
+ "collects = Resource": hasCollects ? collects : "",
1093
+ "import = Resource": hasCollects ? collectsImport : ""
1094
+ }, options);
1095
+ return {
1096
+ name: resourceName,
1097
+ path
1098
+ };
1099
+ }
1100
+ };
1101
+
1102
+ //#endregion
1103
+ //#region src/cli/commands/InitCommand.ts
1104
+ var InitCommand = class extends Command {
1105
+ signature = `init
3
1106
  {--force : Force overwrite if config file already exists (existing file will be backed up) }
4
- `;description=`Initialize Resora`;async handle(){this.app.command=this,this.app.init(),this.success(`Resora initialized`)}},Be=class extends d{signature=`#create:
1107
+ `;
1108
+ description = "Initialize Resora";
1109
+ async handle() {
1110
+ this.app.command = this;
1111
+ this.app.init();
1112
+ this.success("Resora initialized");
1113
+ }
1114
+ };
1115
+
1116
+ //#endregion
1117
+ //#region src/cli/commands/MakeResource.ts
1118
+ var MakeResource = class extends Command {
1119
+ signature = `#create:
5
1120
  {resource : Generates a new resource file.
6
1121
  | {name : Name of the resource to create}
7
1122
  | {--c|collection : Make a resource collection}
@@ -15,11 +1130,1492 @@ import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,rmSync
15
1130
  | {prefix : prefix of the resources to create, "Admin" will create AdminResource, AdminCollection}
16
1131
  | {--force : Create the resource or collection file even if it already exists.}
17
1132
  }
18
- `;description=`Create a new resource or resource collection file`;async handle(){this.app.command=this;let e=``,t=this.argument(`name`)||this.argument(`prefix`),n=this.dictionary.name||this.dictionary.baseCommand;if([`resource`,`collection`].includes(n)&&!t)return void this.error(`Error: Name argument is required.`);if(n===`all`&&!t)return void this.error(`Error: Prefix argument is required.`);switch(n){case`resource`:({path:e}=this.app.makeResource(t,this.options()));break;case`collection`:({path:e}=this.app.makeResource(t+`Collection`,this.options()));break;case`all`:{let n=this.app.makeResource(t,{force:this.option(`force`)}),r=this.app.makeResource(t+`Collection`,{collection:!0,force:this.option(`force`)});e=`${n.path}, ${r.path}`;break}default:this.fail(`Unknown action: ${n}`)}this.success(`Created: ${e}`)}};String.raw`
1133
+ `;
1134
+ description = "Create a new resource or resource collection file";
1135
+ async handle() {
1136
+ this.app.command = this;
1137
+ let path = "";
1138
+ const name = this.argument("name") || this.argument("prefix");
1139
+ const action = this.dictionary.name || this.dictionary.baseCommand;
1140
+ if (["resource", "collection"].includes(action) && !name) return void this.error("Error: Name argument is required.");
1141
+ if (action === "all" && !name) return void this.error("Error: Prefix argument is required.");
1142
+ switch (action) {
1143
+ case "resource":
1144
+ ({path} = this.app.makeResource(name, this.options()));
1145
+ break;
1146
+ case "collection":
1147
+ ({path} = this.app.makeResource(name + "Collection", this.options()));
1148
+ break;
1149
+ case "all": {
1150
+ const o1 = this.app.makeResource(name, { force: this.option("force") });
1151
+ const o2 = this.app.makeResource(name + "Collection", {
1152
+ collection: true,
1153
+ force: this.option("force")
1154
+ });
1155
+ path = `${o1.path}, ${o2.path}`;
1156
+ break;
1157
+ }
1158
+ default: this.fail(`Unknown action: ${action}`);
1159
+ }
1160
+ this.success(`Created: ${path}`);
1161
+ }
1162
+ };
1163
+
1164
+ //#endregion
1165
+ //#region src/cli/logo.ts
1166
+ var logo_default = String.raw`
19
1167
  _____
20
1168
  | __ \
21
1169
  | |__) |___ ___ ___ _ __ __ _
22
1170
  | _ // _ \/ __|/ _ \| '__/ _, |
23
1171
  | | \ \ __/\__ \ (_) | | | (_| |
24
1172
  |_| \_\___||___/\___/|_| \__,_|
25
- `;var Q=class{_status=200;headers={};constructor(e,t){this.response=e,this.body=t}setStatusCode(e){return this._status=e,this}setBody(e){return this.body=e,this}status(){return this._status}statusText(){if(`statusMessage`in this.response)return this.response.statusMessage;if(`statusText`in this.response)return this.response.statusText}setCookie(e,t,n){return this.#e(`Set-Cookie`,`${e}=${t}; ${Object.entries(n||{}).map(([e,t])=>`${e}=${t}`).join(`; `)}`),this}setHeaders(e){for(let[t,n]of Object.entries(e))this.#e(t,n);return this}header(e,t){return this.#e(e,t),this}#e(e,t){this.headers[e]=t,`headers`in this.response?this.response.headers.set(e,t):`setHeader`in this.response&&this.response.setHeader(e,t)}send(e){if(e!==void 0&&(this.body=e),`send`in this.response&&typeof this.response.send==`function`){`statusCode`in this.response&&(this.response.statusCode=this._status);let e=this.response.send(this.body);return e&&`status`in e&&typeof e.status==`function`?e.status(this._status):`status`in this.response&&typeof this.response.status==`function`&&this.response.status(this._status),this.body}return`status`in this.response&&typeof this.response.status!=`function`&&(this.response.status=this._status),this.body}then(e,t){return Promise.resolve(this.send()).then(e,t)}catch(e){return this.then(void 0,e)}finally(e){return this.then(e,e)}},$=class{static preferredCase;static responseStructure;static config;static ctx;instanceConfig;additionalMeta;called={};constructor(){Z()}static setCtx(e){k(e),this.ctx=e}when(e,t){return H(e,t)}whenNotNull(e){return Ee(e)}mergeWhen(e,t){return De(e,t)}with(e){if(this.called.with=!0,e===void 0)return this.additionalMeta||{};let t=typeof e==`function`?e(this.getResourceForMeta()):e;return this.additionalMeta=M(this.additionalMeta,t),this.called.json&&this.applyMetaToBody(t,this.resolveCurrentRootKey()),this}withMeta(e){return this.with(e),this}resolveMergedMeta(e){return M(Te(this,e),this.additionalMeta)}runResponse(e){this.called.toResponse=!0,e.ensureJson();let t=e.body(),n=e.createServerResponse(e.rawResponse,t);return this.called.withResponse=!0,e.callWithResponse(n,e.rawResponse),typeof n?.setBody==`function`&&n.setBody(e.body()),n}runThen(e){this.called.then=!0,e.ensureJson();let t=e.body(),n;e.rawResponse===void 0?(this.called.withResponse=!0,e.callWithResponse()):(n=e.createServerResponse(e.rawResponse,t),this.called.withResponse=!0,e.callWithResponse(n,e.rawResponse));let r=e.body();typeof n?.setBody==`function`&&n.setBody(r);let i=Promise.resolve(r).then(e.onfulfilled,e.onrejected);return typeof n?.send==`function`?n.send(r):e.rawResponse!==void 0&&e.sendRawResponse&&e.sendRawResponse(e.rawResponse,r),i}config(e){return e===void 0?this.instanceConfig||{}:(this.instanceConfig={...this.instanceConfig||{},...e,responseStructure:{...this.instanceConfig?.responseStructure||{},...e.responseStructure||{}}},this)}resolveSerializerConfig(e,t){let n=typeof e.config==`function`?e.config():{};return{preferredCase:this.instanceConfig?.preferredCase??n?.preferredCase,responseStructure:{...n?.responseStructure||{},...this.instanceConfig?.responseStructure||{}}}}resolveSerializerCaseStyle(e,t){return this.resolveSerializerConfig(e,t).preferredCase??e.preferredCase??t?.preferredCase??ne()}resolveSerializerResponseStructure(e,t){let n=this.resolveSerializerConfig(e,t),r=ie();return{wrap:n.responseStructure?.wrap??e.responseStructure?.wrap??t?.responseStructure?.wrap??r?.wrap??!0,rootKey:n.responseStructure?.rootKey??e.responseStructure?.rootKey??t?.responseStructure?.rootKey??r?.rootKey??`data`,factory:n.responseStructure?.factory??e.responseStructure?.factory??t?.responseStructure?.factory??r?.factory}}},Ve=class e extends ${body={data:{}};res;resource;collects;withResponseContext;constructor(t,n){if(super(),n&&(e.ctx=n),this.resource=t,n){let e=E(n);e&&w(e),this.res=O(n)}let r=!!this.resource&&typeof this.resource==`object`&&`data`in this.resource,i=r?this.resource.data:void 0,a=!!i&&!Array.isArray(i),o=r?i:this.resource;if(o&&typeof o==`object`&&!Array.isArray(o)&&!P(o)){let e=N(o)?Object.keys(o.toObject()):Object.keys(o);for(let t of e)t in this||Object.defineProperty(this,t,{enumerable:!0,configurable:!0,get:()=>N(o)&&typeof o.getAttribute==`function`?o.getAttribute(t):a?i[t]:this.resource[t],set:e=>{if(N(o)&&typeof o.setAttribute==`function`){o.setAttribute(t,e);return}if(a){i[t]=e;return}this.resource[t]=e}})}}data(){return this.resource}getBody(){return this.json(),this.body}setBody(e){return this.body=e,this}resolveCollectsConfig(){let e=this.collects;if(!e)return;let t=typeof e.config==`function`?e.config():{};return{preferredCase:t.preferredCase??e.preferredCase,responseStructure:{...e.responseStructure||{},...t.responseStructure||{}}}}resolveResponseStructure(){return this.resolveSerializerResponseStructure(this.constructor,this.resolveCollectsConfig())}resolveCurrentRootKey(){return this.resolveResponseStructure().rootKey}applyMetaToBody(e,t){this.body=j(this.body,e,t)}getResourceForMeta(){return this.resource}getPayloadKey(){let{wrap:e,rootKey:t,factory:n}=this.resolveResponseStructure();return n||!e?void 0:t}json(){if(!this.called.json){this.called.json=!0;let t=F(this.data());Array.isArray(t)&&this.collects&&(t=t.map(e=>new this.collects(e).data())),!Array.isArray(t)&&t&&t.data!==void 0&&(t=t.data),t=U(t);let n=z(this.resource),{metaKey:r}=I(),i=r?n[r]:void 0;r&&delete n[r];let a=this.resolveSerializerCaseStyle(this.constructor,this.resolveCollectsConfig());if(a){let e=G(a);t=K(t,e)}let o=this.resolveMergedMeta(e.prototype.with),{wrap:s,rootKey:c,factory:l}=this.resolveResponseStructure();this.body=B({payload:t,meta:i,metaKey:r,wrap:s,rootKey:c,factory:l,context:{type:`generic`,resource:this.resource}}),this.body=j(this.body,{...n,...o||{}},c)}return this}toObject(){this.called.toObject=!0,this.json();let e=F(this.resource);return!Array.isArray(e)&&e&&e.data!==void 0&&(e=e.data),e}toArray(){return this.called.toArray=!0,this.toObject()}additional(e){this.called.additional=!0,this.json();let t=e.data;delete e.data,delete e.pagination;let n=this.getPayloadKey();return t&&n&&this.body[n]!==void 0&&(this.body[n]=Array.isArray(this.body[n])?[...this.body[n],...t]:{...this.body[n],...t}),this.body={...this.body,...e},this}response(t){let n=t??this.res??e.ctx?.res??e.ctx;return this.runResponse({ensureJson:()=>this.json(),rawResponse:n,body:()=>this.body,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)}})}withResponse(e,t){return this}then(t,n){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:n})}catch(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onrejected:t})}finally(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:t})}},He=class e extends ${body={data:[]};res;resource;collects;withResponseContext;isPaginatedCollectible(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.pagination&&Array.isArray(t.data)?!0:t.meta&&typeof t.meta==`object`&&`currentPage`in t.meta?Array.isArray(t.data)||P(t.data):!1}constructor(t,n){if(super(),n&&(e.ctx=n),this.resource=t,n){let e=E(n);e&&w(e),this.res=O(n)}}data(){return this.toObject()}getBody(){return this.json(),this.body}setBody(e){return this.body=e,this}resolveCollectsConfig(){let e=this.collects;if(!e)return;let t=typeof e.config==`function`?e.config():{};return{preferredCase:t.preferredCase??e.preferredCase,responseStructure:{...e.responseStructure||{},...t.responseStructure||{}}}}resolveResponseStructure(){return this.resolveSerializerResponseStructure(this.constructor,this.resolveCollectsConfig())}resolveCurrentRootKey(){return this.resolveResponseStructure().rootKey}applyMetaToBody(e,t){this.body=j(this.body,e,t)}getResourceForMeta(){return this.resource}getPayloadKey(){let{wrap:e,rootKey:t,factory:n}=this.resolveResponseStructure();return n||!e?void 0:t}json(){if(!this.called.json){this.called.json=!0;let t=this.data();this.collects&&(t=t.map(e=>new this.collects(e).data())),t=F(t),t=U(t);let n=Array.isArray(this.resource)?{}:z(this.resource),{metaKey:r}=I(),i=r?n[r]:void 0;r&&delete n[r];let a=this.resolveSerializerCaseStyle(this.constructor,this.resolveCollectsConfig());if(a){let e=G(a);t=K(t,e)}let o=this.resolveMergedMeta(e.prototype.with),{wrap:s,rootKey:c,factory:l}=this.resolveResponseStructure();this.body=B({payload:t,meta:i,metaKey:r,wrap:s,rootKey:c,factory:l,context:{type:`collection`,resource:this.resource}}),this.body=j(this.body,{...n,...o||{}},c)}return this}toObject(){return this.called.toObject=!0,this.json(),F(Array.isArray(this.resource)?this.resource:P(this.resource)?this.resource.all():this.resource.data)}toArray(){return this.called.toArray=!0,this.toObject()}additional(e){this.called.additional=!0,this.json(),delete e.cursor,delete e.pagination;let t=this.getPayloadKey();return e.data&&t&&Array.isArray(this.body[t])&&(this.body[t]=[...this.body[t],...e.data]),this.body={...this.body,...e},this}response(t){let n=t??this.res??e.ctx?.res??e.ctx;return this.runResponse({ensureJson:()=>this.json(),rawResponse:n,body:()=>this.body,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)}})}withResponse(e,t){return this}setCollects(e){return this.collects=e,this}then(t,n){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:n})}catch(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onrejected:t})}finally(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:t})}},Ue=class e extends ${body={data:{}};res;resource;withResponseContext;constructor(t,n){if(super(),n&&(e.ctx=n),this.resource=t,n){let e=E(n);e&&w(e),this.res=O(n)}let r=this.resource.data??this.resource;if(!Array.isArray(r)){let e=N(r)?Object.keys(r.toObject()):Object.keys(r);for(let t of e)t in this||Object.defineProperty(this,t,{enumerable:!0,configurable:!0,get:()=>N(r)&&typeof r.getAttribute==`function`?r.getAttribute(t):this.resource.data?.[t]??this.resource[t],set:e=>{if(N(r)&&typeof r.setAttribute==`function`){r.setAttribute(t,e);return}this.resource.data&&this.resource.data[t]?this.resource.data[t]=e:this.resource[t]=e}})}}static collection(e){return new He(e).setCollects(this)}data(){return this.toObject()}getBody(){return this.json(),this.body}setBody(e){return this.body=e,this}resolveResponseStructure(){return this.resolveSerializerResponseStructure(this.constructor)}resolveCurrentRootKey(){return this.resolveResponseStructure().rootKey}applyMetaToBody(e,t){this.body=j(this.body,e,t)}getResourceForMeta(){return this.resource}getPayloadKey(){let{wrap:e,rootKey:t,factory:n}=this.resolveResponseStructure();return n||!e?void 0:t}json(){if(!this.called.json){this.called.json=!0;let t=F(this.data());!Array.isArray(t)&&t&&t.data!==void 0&&(t=t.data),t=U(t);let n=this.resolveSerializerCaseStyle(this.constructor);if(n){let e=G(n);t=K(t,e)}let r=this.resolveMergedMeta(e.prototype.with),{wrap:i,rootKey:a,factory:o}=this.resolveResponseStructure();this.body=B({payload:t,wrap:i,rootKey:a,factory:o,context:{type:`resource`,resource:this.resource}}),this.body=j(this.body,r,a)}return this}toObject(){this.called.toObject=!0,this.json();let e=F(this.resource);return!Array.isArray(e)&&e&&e.data!==void 0&&(e=e.data),e}toArray(){return this.called.toArray=!0,this.toObject()}additional(e){this.called.additional=!0,this.json();let t=this.getPayloadKey();return e.data&&t&&this.body[t]!==void 0&&(this.body[t]=Array.isArray(this.body[t])?[...this.body[t],...e.data]:{...this.body[t],...e.data}),this.body={...this.body,...e},this}response(t){let n=t??this.res??e.ctx?.res??e.ctx;return this.runResponse({ensureJson:()=>this.json(),rawResponse:n,body:()=>this.body,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)}})}withResponse(e,t){return this}then(t,n){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:n})}catch(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onrejected:t})}finally(t){return this.runThen({ensureJson:()=>this.json(),body:()=>this.body,rawResponse:this.res??e.ctx?.res??e.ctx,createServerResponse:(e,t)=>{let n=new Q(e,t);return this.withResponseContext={response:n,raw:e},n},callWithResponse:(e,t)=>{this.withResponse(e,t)},sendRawResponse:(e,t)=>{e.send(t)},onfulfilled:t,onrejected:t})}};export{f as ApiResource,V as CONDITIONAL_ATTRIBUTE_MISSING,Re as CliApp,Ve as GenericResource,ze as InitCommand,Be as MakeResource,Ue as Resource,He as ResourceCollection,Q as ServerResponse,j as appendRootProperties,Pe as applyRuntimeConfig,z as buildPaginationExtras,B as buildResponseEnvelope,Ce as createArkormCurrentPageResolver,J as defineConfig,E as extractRequestUrl,O as extractResponseFromCtx,G as getCaseTransformer,Me as getDefaultConfig,pe as getGlobalBaseUrl,ne as getGlobalCase,ve as getGlobalCursorMeta,C as getGlobalPageName,fe as getGlobalPaginatedExtras,x as getGlobalPaginatedLinks,ge as getGlobalPaginatedMeta,ue as getGlobalResponseFactory,ce as getGlobalResponseRootKey,ie as getGlobalResponseStructure,se as getGlobalResponseWrap,I as getPaginationExtraKeys,T as getRequestUrl,R as hasPaginationLink,P as isArkormLikeCollection,N as isArkormLikeModel,A as isPlainObject,Z as loadRuntimeConfig,M as mergeMetadata,F as normalizeSerializableData,Ne as resetRuntimeConfigForTests,Se as resolveCurrentPage,De as resolveMergeWhen,H as resolveWhen,Ee as resolveWhenNotNull,Te as resolveWithHookMetadata,U as sanitizeConditionalAttributes,k as setCtx,S as setGlobalBaseUrl,te as setGlobalCase,_e as setGlobalCursorMeta,me as setGlobalPageName,de as setGlobalPaginatedExtras,b as setGlobalPaginatedLinks,he as setGlobalPaginatedMeta,le as setGlobalResponseFactory,ae as setGlobalResponseRootKey,re as setGlobalResponseStructure,oe as setGlobalResponseWrap,w as setRequestUrl,W as splitWords,Oe as toCamelCase,je as toKebabCase,Ae as toPascalCase,ke as toSnakeCase,K as transformKeys};
1173
+ `;
1174
+
1175
+ //#endregion
1176
+ //#region src/plugins.ts
1177
+ const registeredPlugins = /* @__PURE__ */ new Map();
1178
+ const registeredUtilities = /* @__PURE__ */ new Map();
1179
+ const getRegisteredPlugins = () => {
1180
+ return Array.from(registeredPlugins.values());
1181
+ };
1182
+ const registerUtility = (name, utility) => {
1183
+ registeredUtilities.set(name, utility);
1184
+ return utility;
1185
+ };
1186
+ const getUtility = (name) => {
1187
+ return registeredUtilities.get(name);
1188
+ };
1189
+ const pluginApi = {
1190
+ runWithCtx,
1191
+ setCtx,
1192
+ getCtx,
1193
+ registerUtility,
1194
+ getUtility,
1195
+ getRegisteredPlugins
1196
+ };
1197
+ const definePlugin = (plugin) => {
1198
+ return plugin;
1199
+ };
1200
+ const registerPlugin = (plugins) => {
1201
+ for (const plugin of Array.isArray(plugins) ? plugins : [plugins]) {
1202
+ if (registeredPlugins.has(plugin.name)) continue;
1203
+ registeredPlugins.set(plugin.name, plugin);
1204
+ plugin.setup?.(pluginApi);
1205
+ }
1206
+ return getRegisteredPlugins();
1207
+ };
1208
+ const runPluginHook = (hook, event) => {
1209
+ for (const plugin of registeredPlugins.values()) {
1210
+ const handler = plugin[hook];
1211
+ if (typeof handler === "function") handler(event, pluginApi);
1212
+ }
1213
+ return event;
1214
+ };
1215
+ const resetPluginsForTests = () => {
1216
+ registeredPlugins.clear();
1217
+ registeredUtilities.clear();
1218
+ };
1219
+
1220
+ //#endregion
1221
+ //#region src/ServerResponse.ts
1222
+ /**
1223
+ * ServerResponse class to handle HTTP response construction and sending, compatible
1224
+ * with both Express and H3 response objects.
1225
+ *
1226
+ * @author Legacy (3m1n3nc3)
1227
+ * @since 0.1.0
1228
+ */
1229
+ var ServerResponse = class {
1230
+ _status = 200;
1231
+ headers = {};
1232
+ constructor(response, body) {
1233
+ this.response = response;
1234
+ this.body = body;
1235
+ }
1236
+ /**
1237
+ * Set the HTTP status code for the response
1238
+ *
1239
+ * @param status
1240
+ * @returns The current ServerResponse instance
1241
+ */
1242
+ setStatusCode(status) {
1243
+ this._status = status;
1244
+ return this;
1245
+ }
1246
+ /**
1247
+ * Replace the response body that will be dispatched.
1248
+ *
1249
+ * @param body
1250
+ * @returns The current ServerResponse instance
1251
+ */
1252
+ setBody(body) {
1253
+ this.body = body;
1254
+ return this;
1255
+ }
1256
+ /**
1257
+ * Get the current HTTP status code for the response
1258
+ *
1259
+ * @returns
1260
+ */
1261
+ status() {
1262
+ return this._status;
1263
+ }
1264
+ /**
1265
+ * Get the current HTTP status text for the response
1266
+ *
1267
+ * @returns
1268
+ */
1269
+ statusText() {
1270
+ if ("statusMessage" in this.response) return this.response.statusMessage;
1271
+ else if ("statusText" in this.response) return this.response.statusText;
1272
+ }
1273
+ /**
1274
+ * Set a cookie in the response header
1275
+ *
1276
+ * @param name The name of the cookie
1277
+ * @param value The value of the cookie
1278
+ * @param options Optional cookie attributes (e.g., path, domain, maxAge)
1279
+ * @returns The current ServerResponse instance
1280
+ */
1281
+ setCookie(name, value, options) {
1282
+ this.#addHeader("Set-Cookie", `${name}=${value}; ${Object.entries(options || {}).map(([key, val]) => `${key}=${val}`).join("; ")}`);
1283
+ return this;
1284
+ }
1285
+ /**
1286
+ * Convert the resource to a JSON response body
1287
+ *
1288
+ * @param headers Optional headers to add to the response
1289
+ * @returns The current ServerResponse instance
1290
+ */
1291
+ setHeaders(headers) {
1292
+ for (const [key, value] of Object.entries(headers)) this.#addHeader(key, value);
1293
+ return this;
1294
+ }
1295
+ /**
1296
+ * Add a single header to the response
1297
+ *
1298
+ * @param key The name of the header
1299
+ * @param value The value of the header
1300
+ * @returns The current ServerResponse instance
1301
+ */
1302
+ header(key, value) {
1303
+ this.#addHeader(key, value);
1304
+ return this;
1305
+ }
1306
+ /**
1307
+ * Add a single header to the response
1308
+ *
1309
+ * @param key The name of the header
1310
+ * @param value The value of the header
1311
+ */
1312
+ #addHeader(key, value) {
1313
+ this.headers[key] = value;
1314
+ if ("headers" in this.response) this.response.headers.set(key, value);
1315
+ else if ("setHeader" in this.response) this.response.setHeader(key, value);
1316
+ }
1317
+ /**
1318
+ * Dispatch the current body and apply any deferred transport state.
1319
+ *
1320
+ * @param body Optional body override
1321
+ * @returns The dispatched response body
1322
+ */
1323
+ send(body) {
1324
+ if (typeof body !== "undefined") this.body = body;
1325
+ const beforeSend = runPluginHook("beforeSend", {
1326
+ response: this,
1327
+ rawResponse: this.response,
1328
+ body: this.body,
1329
+ status: this._status,
1330
+ headers: { ...this.headers }
1331
+ });
1332
+ this.body = beforeSend.body;
1333
+ this._status = beforeSend.status;
1334
+ this.headers = { ...beforeSend.headers };
1335
+ if ("send" in this.response && typeof this.response.send === "function") {
1336
+ if ("statusCode" in this.response) this.response.statusCode = this._status;
1337
+ const sentResponse = this.response.send(this.body);
1338
+ if (sentResponse && "status" in sentResponse && typeof sentResponse.status === "function") sentResponse.status(this._status);
1339
+ else if ("status" in this.response && typeof this.response.status === "function") this.response.status(this._status);
1340
+ runPluginHook("afterSend", {
1341
+ response: this,
1342
+ rawResponse: this.response,
1343
+ body: this.body,
1344
+ status: this._status,
1345
+ headers: { ...this.headers }
1346
+ });
1347
+ return this.body;
1348
+ }
1349
+ if ("status" in this.response && typeof this.response.status !== "function") this.response.status = this._status;
1350
+ runPluginHook("afterSend", {
1351
+ response: this,
1352
+ rawResponse: this.response,
1353
+ body: this.body,
1354
+ status: this._status,
1355
+ headers: { ...this.headers }
1356
+ });
1357
+ return this.body;
1358
+ }
1359
+ /**
1360
+ * Promise-like then method to allow chaining with async/await or .then() syntax
1361
+ *
1362
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
1363
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
1364
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
1365
+ */
1366
+ then(onfulfilled, onrejected) {
1367
+ return Promise.resolve(this.send()).then(onfulfilled, onrejected);
1368
+ }
1369
+ /**
1370
+ * Promise-like catch method to handle rejected state of the promise
1371
+ *
1372
+ * @param onrejected
1373
+ * @returns
1374
+ */
1375
+ catch(onrejected) {
1376
+ return this.then(void 0, onrejected);
1377
+ }
1378
+ /**
1379
+ * Promise-like finally method to handle cleanup after promise is settled
1380
+ *
1381
+ * @param onfinally
1382
+ * @returns
1383
+ */
1384
+ finally(onfinally) {
1385
+ return this.then(onfinally, onfinally);
1386
+ }
1387
+ };
1388
+
1389
+ //#endregion
1390
+ //#region src/BaseSerializer.ts
1391
+ /**
1392
+ * @description BaseSerializer is an abstract class that provides common functionality for
1393
+ * serializing resources. It handles configuration, metadata management, and response
1394
+ * structure resolution. Concrete serializers should extend this class and implement
1395
+ * the abstract methods to define how resources are transformed and how metadata is
1396
+ * applied to the response body.
1397
+ *
1398
+ * @author Legacy (3m1n3nc3)
1399
+ * @since 0.2.8
1400
+ */
1401
+ var BaseSerializer = class {
1402
+ static preferredCase;
1403
+ static responseStructure;
1404
+ static config;
1405
+ static ctx;
1406
+ instanceConfig;
1407
+ additionalMeta;
1408
+ called = {};
1409
+ constructor() {
1410
+ loadRuntimeConfig();
1411
+ }
1412
+ /**
1413
+ * Sets the current request context for pagination URL detection.
1414
+ * Call from middleware to make the request path available to all
1415
+ * resources created during the request lifecycle.
1416
+ *
1417
+ * Accepts an Express Request, H3Event, `{ req }` object, or a plain URL string.
1418
+ *
1419
+ * @param ctx The request context.
1420
+ */
1421
+ static setCtx(ctx) {
1422
+ setCtx(ctx);
1423
+ this.ctx = ctx;
1424
+ }
1425
+ /**
1426
+ * Helper method to conditionally resolve a value based on a condition.
1427
+ *
1428
+ * @param condition
1429
+ * @param value
1430
+ * @returns
1431
+ */
1432
+ when(condition, value) {
1433
+ return resolveWhen(condition, value);
1434
+ }
1435
+ /**
1436
+ * Helper method to conditionally resolve a value only if it's not null or undefined.
1437
+ *
1438
+ * @param value
1439
+ * @returns
1440
+ */
1441
+ whenNotNull(value) {
1442
+ return resolveWhenNotNull(value);
1443
+ }
1444
+ /**
1445
+ * Helper method to conditionally merge values into the response based on a condition.
1446
+ *
1447
+ * @param condition
1448
+ * @param value
1449
+ * @returns
1450
+ */
1451
+ mergeWhen(condition, value) {
1452
+ return resolveMergeWhen(condition, value);
1453
+ }
1454
+ /**
1455
+ * Apply registered plugins for the serialization process, allowing plugins to
1456
+ * modify the response body and metadata before the response is sent.
1457
+ *
1458
+ * @param body
1459
+ * @returns
1460
+ */
1461
+ applySerializePlugins(body) {
1462
+ return runPluginHook("afterSerialize", runPluginHook("beforeSerialize", {
1463
+ serializer: this,
1464
+ serializerType: this.getSerializerType(),
1465
+ resource: this.getResourceForMeta(),
1466
+ body
1467
+ })).body;
1468
+ }
1469
+ /**
1470
+ * Apply registered plugins for the response process, allowing plugins to modify the
1471
+ * response body, headers, and status before the response is sent.
1472
+ *
1473
+ * @param input
1474
+ * @returns
1475
+ */
1476
+ applyResponsePlugins(input) {
1477
+ const beforeResponse = runPluginHook("beforeResponse", {
1478
+ serializer: this,
1479
+ serializerType: this.getSerializerType(),
1480
+ rawResponse: input.rawResponse,
1481
+ response: input.response,
1482
+ body: input.body
1483
+ });
1484
+ this.setBody(beforeResponse.body);
1485
+ if (typeof input.response?.setBody === "function") input.response.setBody(beforeResponse.body);
1486
+ const afterResponse = runPluginHook("afterResponse", beforeResponse);
1487
+ this.setBody(afterResponse.body);
1488
+ if (typeof input.response?.setBody === "function") input.response.setBody(afterResponse.body);
1489
+ return afterResponse.body;
1490
+ }
1491
+ /**
1492
+ * Resolve the raw response object from the provided response or the current
1493
+ * context, allowing plugins to access and modify the raw response before it is sent.
1494
+ *
1495
+ * @param response
1496
+ * @returns
1497
+ */
1498
+ resolveRawResponse(response) {
1499
+ if (typeof response !== "undefined") return response;
1500
+ return extractResponseFromCtx(getCtx() ?? this.constructor.ctx);
1501
+ }
1502
+ /**
1503
+ * Dispatch a body to a raw response object when it exposes a send() transport method.
1504
+ *
1505
+ * @param raw
1506
+ * @param body
1507
+ */
1508
+ sendRawResponseBody(raw, body) {
1509
+ if (raw && typeof raw.send === "function") raw.send(body);
1510
+ }
1511
+ /**
1512
+ * Add additional metadata to the response. If called without arguments.
1513
+ *
1514
+ * @param meta
1515
+ * @returns
1516
+ */
1517
+ with(meta) {
1518
+ this.called.with = true;
1519
+ if (typeof meta === "undefined") return this.additionalMeta || {};
1520
+ const resolvedMeta = typeof meta === "function" ? meta(this.getResourceForMeta()) : meta;
1521
+ this.additionalMeta = mergeMetadata(this.additionalMeta, resolvedMeta);
1522
+ if (this.called.json) this.applyMetaToBody(resolvedMeta, this.resolveCurrentRootKey());
1523
+ return this;
1524
+ }
1525
+ /**
1526
+ * Add additional metadata to the response, ensuring it is merged with any existing metadata.
1527
+ *
1528
+ * @param meta
1529
+ * @returns
1530
+ */
1531
+ withMeta(meta) {
1532
+ this.with(meta);
1533
+ return this;
1534
+ }
1535
+ /**
1536
+ * Resolve the merged metadata for the current response, combining any metadata
1537
+ * defined in hooks with additional metadata added via with() or withMeta().
1538
+ *
1539
+ * @param withMethod
1540
+ * @returns
1541
+ */
1542
+ resolveMergedMeta(withMethod) {
1543
+ return mergeMetadata(resolveWithHookMetadata(this, withMethod), this.additionalMeta);
1544
+ }
1545
+ /**
1546
+ * Run the response generation process, ensuring that the response is properly structured.
1547
+ *
1548
+ * @param input
1549
+ * @returns
1550
+ */
1551
+ runResponse(input) {
1552
+ this.called.toResponse = true;
1553
+ input.ensureJson();
1554
+ const resolvedBody = input.body();
1555
+ const response = input.createServerResponse(input.rawResponse, resolvedBody);
1556
+ this.called.withResponse = true;
1557
+ input.callWithResponse(response, input.rawResponse);
1558
+ if (typeof response?.setBody === "function") response.setBody(input.body());
1559
+ this.applyResponsePlugins({
1560
+ body: input.body(),
1561
+ rawResponse: input.rawResponse,
1562
+ response
1563
+ });
1564
+ return response;
1565
+ }
1566
+ /**
1567
+ * Run the thenable process for the resource, allowing for async handling and
1568
+ * response generation.
1569
+ *
1570
+ * @param input
1571
+ * @returns
1572
+ */
1573
+ runThen(input) {
1574
+ this.called.then = true;
1575
+ input.ensureJson();
1576
+ const initialBody = input.body();
1577
+ let response;
1578
+ if (typeof input.rawResponse !== "undefined") {
1579
+ response = input.createServerResponse(input.rawResponse, initialBody);
1580
+ this.called.withResponse = true;
1581
+ input.callWithResponse(response, input.rawResponse);
1582
+ } else {
1583
+ this.called.withResponse = true;
1584
+ input.callWithResponse();
1585
+ }
1586
+ const resolvedBody = input.body();
1587
+ if (typeof response?.setBody === "function") response.setBody(resolvedBody);
1588
+ const dispatchedBody = this.applyResponsePlugins({
1589
+ body: resolvedBody,
1590
+ rawResponse: input.rawResponse,
1591
+ response
1592
+ });
1593
+ const resolved = Promise.resolve(dispatchedBody).then(input.onfulfilled, input.onrejected);
1594
+ if (typeof response?.send === "function") response.send(dispatchedBody);
1595
+ else if (typeof input.rawResponse !== "undefined" && input.sendRawResponse) input.sendRawResponse(input.rawResponse, dispatchedBody);
1596
+ return resolved;
1597
+ }
1598
+ /**
1599
+ * Get or set the resource-level configuration for this serializer instance.
1600
+ *
1601
+ * @param config The configuration object to set for this serializer instance, or undefined to get the current configuration.
1602
+ * @returns
1603
+ */
1604
+ config(config) {
1605
+ if (typeof config === "undefined") return this.instanceConfig || {};
1606
+ this.instanceConfig = {
1607
+ ...this.instanceConfig || {},
1608
+ ...config,
1609
+ responseStructure: {
1610
+ ...this.instanceConfig?.responseStructure || {},
1611
+ ...config.responseStructure || {}
1612
+ }
1613
+ };
1614
+ return this;
1615
+ }
1616
+ /**
1617
+ * Resolve the effective serializer configuration for this instance, combining
1618
+ * class-level and instance-level configurations.
1619
+ *
1620
+ * @param localConstructor
1621
+ * @param _fallbackConfig
1622
+ * @returns
1623
+ */
1624
+ resolveSerializerConfig(localConstructor, _fallbackConfig) {
1625
+ const classConfig = typeof localConstructor.config === "function" ? localConstructor.config() : {};
1626
+ return {
1627
+ preferredCase: this.instanceConfig?.preferredCase ?? classConfig?.preferredCase,
1628
+ responseStructure: {
1629
+ ...classConfig?.responseStructure || {},
1630
+ ...this.instanceConfig?.responseStructure || {}
1631
+ }
1632
+ };
1633
+ }
1634
+ /**
1635
+ * Resolve the preferred case style for this serializer instance, considering
1636
+ * instance-level configuration, class-level configuration, and global defaults.
1637
+ *
1638
+ * @param localConstructor The constructor of the serializer class.
1639
+ * @param fallbackConfig The fallback configuration to use if no other configuration is found.
1640
+ * @returns The resolved case style for this serializer instance.
1641
+ */
1642
+ resolveSerializerCaseStyle(localConstructor, fallbackConfig) {
1643
+ return this.resolveSerializerConfig(localConstructor, fallbackConfig).preferredCase ?? localConstructor.preferredCase ?? fallbackConfig?.preferredCase ?? getGlobalCase();
1644
+ }
1645
+ /**
1646
+ * Resolve the response structure configuration for this serializer instance, considering
1647
+ *
1648
+ * @param localConstructor
1649
+ * @param fallbackConfig
1650
+ * @returns
1651
+ */
1652
+ resolveSerializerResponseStructure(localConstructor, fallbackConfig) {
1653
+ const localConfig = this.resolveSerializerConfig(localConstructor, fallbackConfig);
1654
+ const global = getGlobalResponseStructure();
1655
+ return {
1656
+ wrap: localConfig.responseStructure?.wrap ?? localConstructor.responseStructure?.wrap ?? fallbackConfig?.responseStructure?.wrap ?? global?.wrap ?? true,
1657
+ rootKey: localConfig.responseStructure?.rootKey ?? localConstructor.responseStructure?.rootKey ?? fallbackConfig?.responseStructure?.rootKey ?? global?.rootKey ?? "data",
1658
+ factory: localConfig.responseStructure?.factory ?? localConstructor.responseStructure?.factory ?? fallbackConfig?.responseStructure?.factory ?? global?.factory
1659
+ };
1660
+ }
1661
+ };
1662
+
1663
+ //#endregion
1664
+ //#region src/GenericResource.ts
1665
+ /**
1666
+ * GenericResource class to handle API resource transformation and response building
1667
+ *
1668
+ * @author Legacy (3m1n3nc3)
1669
+ * @since 0.1.0
1670
+ * @see BaseSerializer for shared serialization logic and configuration handling
1671
+ */
1672
+ var GenericResource = class GenericResource extends BaseSerializer {
1673
+ body = { data: {} };
1674
+ res;
1675
+ resource;
1676
+ collects;
1677
+ withResponseContext;
1678
+ constructor(rsc, ctx) {
1679
+ super();
1680
+ if (ctx) GenericResource.ctx = ctx;
1681
+ this.resource = rsc;
1682
+ if (ctx) {
1683
+ const url = extractRequestUrl(ctx);
1684
+ if (url) setRequestUrl(url);
1685
+ this.res = extractResponseFromCtx(ctx);
1686
+ }
1687
+ const hasDataPayload = !!this.resource && typeof this.resource === "object" && "data" in this.resource;
1688
+ const dataPayload = hasDataPayload ? this.resource.data : void 0;
1689
+ const hasObjectDataPayload = !!dataPayload && !Array.isArray(dataPayload);
1690
+ const source = hasDataPayload ? dataPayload : this.resource;
1691
+ /**
1692
+ * Copy properties from rsc to this instance for easy
1693
+ * access, but only if data is not an array
1694
+ */
1695
+ if (source && typeof source === "object" && !Array.isArray(source) && !isArkormLikeCollection(source)) {
1696
+ const sourceKeys = isArkormLikeModel(source) ? Object.keys(source.toObject()) : Object.keys(source);
1697
+ for (const key of sourceKeys) if (!(key in this)) Object.defineProperty(this, key, {
1698
+ enumerable: true,
1699
+ configurable: true,
1700
+ get: () => {
1701
+ if (isArkormLikeModel(source) && typeof source.getAttribute === "function") return source.getAttribute(key);
1702
+ if (hasObjectDataPayload) return dataPayload[key];
1703
+ return this.resource[key];
1704
+ },
1705
+ set: (value) => {
1706
+ if (isArkormLikeModel(source) && typeof source.setAttribute === "function") {
1707
+ source.setAttribute(key, value);
1708
+ return;
1709
+ }
1710
+ if (hasObjectDataPayload) {
1711
+ dataPayload[key] = value;
1712
+ return;
1713
+ }
1714
+ this.resource[key] = value;
1715
+ }
1716
+ });
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Get the original resource data
1721
+ */
1722
+ data() {
1723
+ return this.resource;
1724
+ }
1725
+ /**
1726
+ * Get the current serialized output body.
1727
+ */
1728
+ getBody() {
1729
+ this.json();
1730
+ return this.body;
1731
+ }
1732
+ /**
1733
+ * Replace the current serialized output body.
1734
+ */
1735
+ setBody(body) {
1736
+ this.body = body;
1737
+ return this;
1738
+ }
1739
+ resolveCollectsConfig() {
1740
+ const collectedResource = this.collects;
1741
+ if (!collectedResource) return;
1742
+ const collectedConfig = typeof collectedResource.config === "function" ? collectedResource.config() : {};
1743
+ return {
1744
+ preferredCase: collectedConfig.preferredCase ?? collectedResource.preferredCase,
1745
+ responseStructure: {
1746
+ ...collectedResource.responseStructure || {},
1747
+ ...collectedConfig.responseStructure || {}
1748
+ }
1749
+ };
1750
+ }
1751
+ resolveResponseStructure() {
1752
+ return this.resolveSerializerResponseStructure(this.constructor, this.resolveCollectsConfig());
1753
+ }
1754
+ /**
1755
+ * Resolve the current root key for the response structure, based on configuration and defaults.
1756
+ *
1757
+ * @returns
1758
+ */
1759
+ resolveCurrentRootKey() {
1760
+ return this.resolveResponseStructure().rootKey;
1761
+ }
1762
+ /**
1763
+ * Apply metadata properties to the response body, ensuring they are merged with.
1764
+ *
1765
+ * @param meta
1766
+ * @param rootKey
1767
+ */
1768
+ applyMetaToBody(meta, rootKey) {
1769
+ this.body = appendRootProperties(this.body, meta, rootKey);
1770
+ }
1771
+ /**
1772
+ * Get the resource data to be used for generating metadata.
1773
+ *
1774
+ * @returns
1775
+ */
1776
+ getResourceForMeta() {
1777
+ return this.resource;
1778
+ }
1779
+ getSerializerType() {
1780
+ return "generic";
1781
+ }
1782
+ getPayloadKey() {
1783
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
1784
+ return factory || !wrap ? void 0 : rootKey;
1785
+ }
1786
+ /**
1787
+ * Convert resource to JSON response format
1788
+ *
1789
+ * @returns
1790
+ */
1791
+ json() {
1792
+ if (!this.called.json) {
1793
+ this.called.json = true;
1794
+ let data = normalizeSerializableData(this.data());
1795
+ if (Array.isArray(data) && this.collects) data = data.map((item) => new this.collects(item).data());
1796
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
1797
+ data = sanitizeConditionalAttributes(data);
1798
+ const paginationExtras = buildPaginationExtras(this.resource);
1799
+ const { metaKey } = getPaginationExtraKeys();
1800
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
1801
+ if (metaKey) delete paginationExtras[metaKey];
1802
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
1803
+ if (caseStyle) {
1804
+ const transformer = getCaseTransformer(caseStyle);
1805
+ data = transformKeys(data, transformer);
1806
+ }
1807
+ const customMeta = this.resolveMergedMeta(GenericResource.prototype.with);
1808
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
1809
+ this.body = buildResponseEnvelope({
1810
+ payload: data,
1811
+ meta: configuredMeta,
1812
+ metaKey,
1813
+ wrap,
1814
+ rootKey,
1815
+ factory,
1816
+ context: {
1817
+ type: "generic",
1818
+ resource: this.resource
1819
+ }
1820
+ });
1821
+ this.body = appendRootProperties(this.body, {
1822
+ ...paginationExtras,
1823
+ ...customMeta || {}
1824
+ }, rootKey);
1825
+ this.body = this.applySerializePlugins(this.body);
1826
+ }
1827
+ return this;
1828
+ }
1829
+ /**
1830
+ * Convert resource to object format (for collections).
1831
+ *
1832
+ * @returns
1833
+ */
1834
+ toObject() {
1835
+ this.called.toObject = true;
1836
+ this.json();
1837
+ let data = normalizeSerializableData(this.resource);
1838
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
1839
+ return data;
1840
+ }
1841
+ /**
1842
+ * Convert resource to object format and return original data.
1843
+ *
1844
+ * @deprecated Use toObject() instead.
1845
+ * @alias toArray
1846
+ * @since 0.2.9
1847
+ */
1848
+ toArray() {
1849
+ this.called.toArray = true;
1850
+ return this.toObject();
1851
+ }
1852
+ /**
1853
+ * Add additional properties to the response body
1854
+ *
1855
+ * @param extra Additional properties to merge into the response body
1856
+ * @returns
1857
+ */
1858
+ additional(extra) {
1859
+ this.called.additional = true;
1860
+ this.json();
1861
+ const extraData = extra.data;
1862
+ delete extra.data;
1863
+ delete extra.pagination;
1864
+ const payloadKey = this.getPayloadKey();
1865
+ if (extraData && payloadKey && typeof this.body[payloadKey] !== "undefined") this.body[payloadKey] = Array.isArray(this.body[payloadKey]) ? [...this.body[payloadKey], ...extraData] : {
1866
+ ...this.body[payloadKey],
1867
+ ...extraData
1868
+ };
1869
+ this.body = {
1870
+ ...this.body,
1871
+ ...extra
1872
+ };
1873
+ return this;
1874
+ }
1875
+ /**
1876
+ * Build a response object, writing to the provided raw response if possible.
1877
+ *
1878
+ * @param res
1879
+ * @returns
1880
+ */
1881
+ response(res) {
1882
+ const rawResponse = this.resolveRawResponse(res ?? this.res);
1883
+ return this.runResponse({
1884
+ ensureJson: () => this.json(),
1885
+ rawResponse,
1886
+ body: () => this.body,
1887
+ createServerResponse: (raw, body) => {
1888
+ const response = new ServerResponse(raw, body);
1889
+ this.withResponseContext = {
1890
+ response,
1891
+ raw
1892
+ };
1893
+ return response;
1894
+ },
1895
+ callWithResponse: (response, raw) => {
1896
+ this.withResponse(response, raw);
1897
+ }
1898
+ });
1899
+ }
1900
+ /**
1901
+ * Customize the outgoing transport response right before dispatch.
1902
+ *
1903
+ * Override in custom classes to mutate headers/status/body.
1904
+ */
1905
+ withResponse(_response, _rawResponse) {
1906
+ return this;
1907
+ }
1908
+ /**
1909
+ * Promise-like then method to allow chaining with async/await or .then() syntax
1910
+ *
1911
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
1912
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
1913
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
1914
+ */
1915
+ then(onfulfilled, onrejected) {
1916
+ return this.runThen({
1917
+ ensureJson: () => this.json(),
1918
+ body: () => this.body,
1919
+ rawResponse: this.resolveRawResponse(this.res),
1920
+ createServerResponse: (raw, body) => {
1921
+ const response = new ServerResponse(raw, body);
1922
+ this.withResponseContext = {
1923
+ response,
1924
+ raw
1925
+ };
1926
+ return response;
1927
+ },
1928
+ callWithResponse: (response, raw) => {
1929
+ this.withResponse(response, raw);
1930
+ },
1931
+ sendRawResponse: (raw, body) => {
1932
+ this.sendRawResponseBody(raw, body);
1933
+ },
1934
+ onfulfilled,
1935
+ onrejected
1936
+ });
1937
+ }
1938
+ /**
1939
+ * Promise-like catch method to handle rejected state of the promise
1940
+ *
1941
+ * @param onrejected
1942
+ * @returns
1943
+ */
1944
+ catch(onrejected) {
1945
+ return this.runThen({
1946
+ ensureJson: () => this.json(),
1947
+ body: () => this.body,
1948
+ rawResponse: this.resolveRawResponse(this.res),
1949
+ createServerResponse: (raw, body) => {
1950
+ const response = new ServerResponse(raw, body);
1951
+ this.withResponseContext = {
1952
+ response,
1953
+ raw
1954
+ };
1955
+ return response;
1956
+ },
1957
+ callWithResponse: (response, raw) => {
1958
+ this.withResponse(response, raw);
1959
+ },
1960
+ sendRawResponse: (raw, body) => {
1961
+ this.sendRawResponseBody(raw, body);
1962
+ },
1963
+ onrejected
1964
+ });
1965
+ }
1966
+ /**
1967
+ * Promise-like finally method to handle cleanup after promise is settled
1968
+ *
1969
+ * @param onfinally
1970
+ * @returns
1971
+ */
1972
+ finally(onfinally) {
1973
+ return this.runThen({
1974
+ ensureJson: () => this.json(),
1975
+ body: () => this.body,
1976
+ rawResponse: this.resolveRawResponse(this.res),
1977
+ createServerResponse: (raw, body) => {
1978
+ const response = new ServerResponse(raw, body);
1979
+ this.withResponseContext = {
1980
+ response,
1981
+ raw
1982
+ };
1983
+ return response;
1984
+ },
1985
+ callWithResponse: (response, raw) => {
1986
+ this.withResponse(response, raw);
1987
+ },
1988
+ sendRawResponse: (raw, body) => {
1989
+ this.sendRawResponseBody(raw, body);
1990
+ },
1991
+ onfulfilled: onfinally,
1992
+ onrejected: onfinally
1993
+ });
1994
+ }
1995
+ };
1996
+
1997
+ //#endregion
1998
+ //#region src/ResourceCollection.ts
1999
+ /**
2000
+ * ResourceCollection class to handle API resource transformation and response building
2001
+ * for collections
2002
+ *
2003
+ * @author Legacy (3m1n3nc3)
2004
+ * @since 0.1.0
2005
+ * @see BaseSerializer for shared serialization logic and configuration handling
2006
+ */
2007
+ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2008
+ body = { data: [] };
2009
+ res;
2010
+ resource;
2011
+ collects;
2012
+ withResponseContext;
2013
+ /**
2014
+ * Type guard to determine if the provided value is a Collectible with pagination information.
2015
+ *
2016
+ * @param value The value to check.
2017
+ * @returns True if the value is a Collectible with pagination information, false otherwise.
2018
+ */
2019
+ isPaginatedCollectible(value) {
2020
+ if (!value || typeof value !== "object") return false;
2021
+ const resource = value;
2022
+ if (resource.pagination && Array.isArray(resource.data)) return true;
2023
+ if (!(!!resource.meta && typeof resource.meta === "object" && "currentPage" in resource.meta)) return false;
2024
+ return Array.isArray(resource.data) || isArkormLikeCollection(resource.data);
2025
+ }
2026
+ constructor(rsc, ctx) {
2027
+ super();
2028
+ if (ctx) ResourceCollection.ctx = ctx;
2029
+ this.resource = rsc;
2030
+ if (ctx) {
2031
+ const url = extractRequestUrl(ctx);
2032
+ if (url) setRequestUrl(url);
2033
+ this.res = extractResponseFromCtx(ctx);
2034
+ }
2035
+ }
2036
+ /**
2037
+ * Get the original resource data
2038
+ */
2039
+ data() {
2040
+ return this.toObject();
2041
+ }
2042
+ /**
2043
+ * Get the current serialized output body.
2044
+ */
2045
+ getBody() {
2046
+ this.json();
2047
+ return this.body;
2048
+ }
2049
+ /**
2050
+ * Replace the current serialized output body.
2051
+ */
2052
+ setBody(body) {
2053
+ this.body = body;
2054
+ return this;
2055
+ }
2056
+ resolveCollectsConfig() {
2057
+ const collectedResource = this.collects;
2058
+ if (!collectedResource) return;
2059
+ const collectedConfig = typeof collectedResource.config === "function" ? collectedResource.config() : {};
2060
+ return {
2061
+ preferredCase: collectedConfig.preferredCase ?? collectedResource.preferredCase,
2062
+ responseStructure: {
2063
+ ...collectedResource.responseStructure || {},
2064
+ ...collectedConfig.responseStructure || {}
2065
+ }
2066
+ };
2067
+ }
2068
+ resolveResponseStructure() {
2069
+ return this.resolveSerializerResponseStructure(this.constructor, this.resolveCollectsConfig());
2070
+ }
2071
+ /**
2072
+ * Resolve the current root key for the response structure, based on configuration and defaults.
2073
+ *
2074
+ * @returns
2075
+ */
2076
+ resolveCurrentRootKey() {
2077
+ return this.resolveResponseStructure().rootKey;
2078
+ }
2079
+ /**
2080
+ * Apply metadata properties to the response body, ensuring they are merged with
2081
+ * any existing properties and respecting the configured root key.
2082
+ *
2083
+ * @param meta
2084
+ * @param rootKey
2085
+ */
2086
+ applyMetaToBody(meta, rootKey) {
2087
+ this.body = appendRootProperties(this.body, meta, rootKey);
2088
+ }
2089
+ /**
2090
+ * Get the resource data to be used for generating metadata, allowing for
2091
+ * customization in subclasses.
2092
+ *
2093
+ * @returns
2094
+ */
2095
+ getResourceForMeta() {
2096
+ return this.resource;
2097
+ }
2098
+ getSerializerType() {
2099
+ return "collection";
2100
+ }
2101
+ /**
2102
+ * Get the appropriate key for the response payload based on the current response
2103
+ * structure configuration.
2104
+ *
2105
+ * @returns The key to use for the response payload, or undefined if no key is needed.
2106
+ */
2107
+ getPayloadKey() {
2108
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2109
+ return factory || !wrap ? void 0 : rootKey;
2110
+ }
2111
+ /**
2112
+ * Convert resource to JSON response format
2113
+ *
2114
+ * @returns
2115
+ */
2116
+ json() {
2117
+ if (!this.called.json) {
2118
+ this.called.json = true;
2119
+ let data = this.data();
2120
+ if (this.collects) data = data.map((item) => new this.collects(item).data());
2121
+ data = normalizeSerializableData(data);
2122
+ data = sanitizeConditionalAttributes(data);
2123
+ const paginationExtras = !Array.isArray(this.resource) ? buildPaginationExtras(this.resource) : {};
2124
+ const { metaKey } = getPaginationExtraKeys();
2125
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
2126
+ if (metaKey) delete paginationExtras[metaKey];
2127
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
2128
+ if (caseStyle) {
2129
+ const transformer = getCaseTransformer(caseStyle);
2130
+ data = transformKeys(data, transformer);
2131
+ }
2132
+ const customMeta = this.resolveMergedMeta(ResourceCollection.prototype.with);
2133
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2134
+ this.body = buildResponseEnvelope({
2135
+ payload: data,
2136
+ meta: configuredMeta,
2137
+ metaKey,
2138
+ wrap,
2139
+ rootKey,
2140
+ factory,
2141
+ context: {
2142
+ type: "collection",
2143
+ resource: this.resource
2144
+ }
2145
+ });
2146
+ this.body = appendRootProperties(this.body, {
2147
+ ...paginationExtras,
2148
+ ...customMeta || {}
2149
+ }, rootKey);
2150
+ this.body = this.applySerializePlugins(this.body);
2151
+ }
2152
+ return this;
2153
+ }
2154
+ /**
2155
+ * Convert resource to object format and return original data.
2156
+ *
2157
+ * @returns
2158
+ */
2159
+ toObject() {
2160
+ this.called.toObject = true;
2161
+ this.json();
2162
+ return normalizeSerializableData(Array.isArray(this.resource) ? this.resource : isArkormLikeCollection(this.resource) ? this.resource.all() : this.resource.data);
2163
+ }
2164
+ /**
2165
+ * Convert resource to object format and return original data.
2166
+ *
2167
+ * @deprecated Use toObject() instead.
2168
+ * @alias toArray
2169
+ * @since 0.2.9
2170
+ */
2171
+ toArray() {
2172
+ this.called.toArray = true;
2173
+ return this.toObject();
2174
+ }
2175
+ /**
2176
+ * Add additional properties to the response body
2177
+ *
2178
+ * @param extra Additional properties to merge into the response body
2179
+ * @returns
2180
+ */
2181
+ additional(extra) {
2182
+ this.called.additional = true;
2183
+ this.json();
2184
+ delete extra.cursor;
2185
+ delete extra.pagination;
2186
+ const payloadKey = this.getPayloadKey();
2187
+ if (extra.data && payloadKey && Array.isArray(this.body[payloadKey])) this.body[payloadKey] = [...this.body[payloadKey], ...extra.data];
2188
+ this.body = {
2189
+ ...this.body,
2190
+ ...extra
2191
+ };
2192
+ return this;
2193
+ }
2194
+ /**
2195
+ * Build a response object, optionally accepting a raw response to mutate in withResponse.
2196
+ *
2197
+ * @param res Optional raw response object (e.g. Express Response or H3Event res)
2198
+ * @returns
2199
+ */
2200
+ response(res) {
2201
+ const rawResponse = this.resolveRawResponse(res ?? this.res);
2202
+ return this.runResponse({
2203
+ ensureJson: () => this.json(),
2204
+ rawResponse,
2205
+ body: () => this.body,
2206
+ createServerResponse: (raw, body) => {
2207
+ const response = new ServerResponse(raw, body);
2208
+ this.withResponseContext = {
2209
+ response,
2210
+ raw
2211
+ };
2212
+ return response;
2213
+ },
2214
+ callWithResponse: (response, raw) => {
2215
+ this.withResponse(response, raw);
2216
+ }
2217
+ });
2218
+ }
2219
+ /**
2220
+ * Customize the outgoing transport response right before dispatch.
2221
+ *
2222
+ * Override in custom classes to mutate headers/status/body.
2223
+ */
2224
+ withResponse(_response, _rawResponse) {
2225
+ return this;
2226
+ }
2227
+ setCollects(collects) {
2228
+ this.collects = collects;
2229
+ return this;
2230
+ }
2231
+ /**
2232
+ * Promise-like then method to allow chaining with async/await or .then() syntax
2233
+ *
2234
+ * @param onfulfilled Callback to handle the fulfilled state of the promise
2235
+ * @param onrejected Callback to handle the rejected state of the promise
2236
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
2237
+ */
2238
+ then(onfulfilled, onrejected) {
2239
+ return this.runThen({
2240
+ ensureJson: () => this.json(),
2241
+ body: () => this.body,
2242
+ rawResponse: this.resolveRawResponse(this.res),
2243
+ createServerResponse: (raw, body) => {
2244
+ const response = new ServerResponse(raw, body);
2245
+ this.withResponseContext = {
2246
+ response,
2247
+ raw
2248
+ };
2249
+ return response;
2250
+ },
2251
+ callWithResponse: (response, raw) => {
2252
+ this.withResponse(response, raw);
2253
+ },
2254
+ sendRawResponse: (raw, body) => {
2255
+ this.sendRawResponseBody(raw, body);
2256
+ },
2257
+ onfulfilled,
2258
+ onrejected
2259
+ });
2260
+ }
2261
+ /**
2262
+ * Promise-like catch method to handle rejected state of the promise
2263
+ *
2264
+ * @param onrejected
2265
+ * @returns
2266
+ */
2267
+ catch(onrejected) {
2268
+ return this.runThen({
2269
+ ensureJson: () => this.json(),
2270
+ body: () => this.body,
2271
+ rawResponse: this.resolveRawResponse(this.res),
2272
+ createServerResponse: (raw, body) => {
2273
+ const response = new ServerResponse(raw, body);
2274
+ this.withResponseContext = {
2275
+ response,
2276
+ raw
2277
+ };
2278
+ return response;
2279
+ },
2280
+ callWithResponse: (response, raw) => {
2281
+ this.withResponse(response, raw);
2282
+ },
2283
+ sendRawResponse: (raw, body) => {
2284
+ this.sendRawResponseBody(raw, body);
2285
+ },
2286
+ onrejected
2287
+ });
2288
+ }
2289
+ /**
2290
+ * Promise-like finally method to handle cleanup after promise is settled
2291
+ *
2292
+ * @param onfinally
2293
+ * @returns
2294
+ */
2295
+ finally(onfinally) {
2296
+ return this.runThen({
2297
+ ensureJson: () => this.json(),
2298
+ body: () => this.body,
2299
+ rawResponse: this.resolveRawResponse(this.res),
2300
+ createServerResponse: (raw, body) => {
2301
+ const response = new ServerResponse(raw, body);
2302
+ this.withResponseContext = {
2303
+ response,
2304
+ raw
2305
+ };
2306
+ return response;
2307
+ },
2308
+ callWithResponse: (response, raw) => {
2309
+ this.withResponse(response, raw);
2310
+ },
2311
+ sendRawResponse: (raw, body) => {
2312
+ this.sendRawResponseBody(raw, body);
2313
+ },
2314
+ onfulfilled: onfinally,
2315
+ onrejected: onfinally
2316
+ });
2317
+ }
2318
+ };
2319
+
2320
+ //#endregion
2321
+ //#region src/Resource.ts
2322
+ /**
2323
+ * Resource class to handle API resource transformation and response building
2324
+ *
2325
+ *
2326
+ * @author Legacy (3m1n3nc3)
2327
+ * @since 0.1.0
2328
+ * @see BaseSerializer for shared serialization logic and configuration handling
2329
+ */
2330
+ var Resource = class Resource extends BaseSerializer {
2331
+ body = { data: {} };
2332
+ res;
2333
+ resource;
2334
+ withResponseContext;
2335
+ constructor(rsc, ctx) {
2336
+ super();
2337
+ if (ctx) Resource.ctx = ctx;
2338
+ this.resource = rsc;
2339
+ if (ctx) {
2340
+ const url = extractRequestUrl(ctx);
2341
+ if (url) setRequestUrl(url);
2342
+ this.res = extractResponseFromCtx(ctx);
2343
+ }
2344
+ const source = this.resource.data ?? this.resource;
2345
+ /**
2346
+ * Copy properties from rsc to this instance for easy
2347
+ * access, but only if data is not an array
2348
+ */
2349
+ if (!Array.isArray(source)) {
2350
+ const sourceKeys = isArkormLikeModel(source) ? Object.keys(source.toObject()) : Object.keys(source);
2351
+ for (const key of sourceKeys) if (!(key in this)) Object.defineProperty(this, key, {
2352
+ enumerable: true,
2353
+ configurable: true,
2354
+ get: () => {
2355
+ if (isArkormLikeModel(source) && typeof source.getAttribute === "function") return source.getAttribute(key);
2356
+ return this.resource.data?.[key] ?? this.resource[key];
2357
+ },
2358
+ set: (value) => {
2359
+ if (isArkormLikeModel(source) && typeof source.setAttribute === "function") {
2360
+ source.setAttribute(key, value);
2361
+ return;
2362
+ }
2363
+ if (this.resource.data && this.resource.data[key]) this.resource.data[key] = value;
2364
+ else this.resource[key] = value;
2365
+ }
2366
+ });
2367
+ }
2368
+ }
2369
+ /**
2370
+ * Create a ResourceCollection from an array of resource data or a Collectible instance
2371
+ *
2372
+ * @param data
2373
+ * @returns
2374
+ */
2375
+ static collection(data) {
2376
+ return new ResourceCollection(data).setCollects(this);
2377
+ }
2378
+ /**
2379
+ * Get the original resource data
2380
+ */
2381
+ data() {
2382
+ return this.toObject();
2383
+ }
2384
+ /**
2385
+ * Get the current serialized output body.
2386
+ */
2387
+ getBody() {
2388
+ this.json();
2389
+ return this.body;
2390
+ }
2391
+ /**
2392
+ * Replace the current serialized output body.
2393
+ */
2394
+ setBody(body) {
2395
+ this.body = body;
2396
+ return this;
2397
+ }
2398
+ resolveResponseStructure() {
2399
+ return this.resolveSerializerResponseStructure(this.constructor);
2400
+ }
2401
+ /**
2402
+ * Resolve the current root key for the response body based on configuration and defaults.
2403
+ *
2404
+ * @returns
2405
+ */
2406
+ resolveCurrentRootKey() {
2407
+ return this.resolveResponseStructure().rootKey;
2408
+ }
2409
+ applyMetaToBody(meta, rootKey) {
2410
+ this.body = appendRootProperties(this.body, meta, rootKey);
2411
+ }
2412
+ getResourceForMeta() {
2413
+ return this.resource;
2414
+ }
2415
+ getSerializerType() {
2416
+ return "resource";
2417
+ }
2418
+ getPayloadKey() {
2419
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2420
+ return factory || !wrap ? void 0 : rootKey;
2421
+ }
2422
+ /**
2423
+ * Convert resource to JSON response format
2424
+ *
2425
+ * @returns
2426
+ */
2427
+ json() {
2428
+ if (!this.called.json) {
2429
+ this.called.json = true;
2430
+ let data = normalizeSerializableData(this.data());
2431
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
2432
+ data = sanitizeConditionalAttributes(data);
2433
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor);
2434
+ if (caseStyle) {
2435
+ const transformer = getCaseTransformer(caseStyle);
2436
+ data = transformKeys(data, transformer);
2437
+ }
2438
+ const customMeta = this.resolveMergedMeta(Resource.prototype.with);
2439
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2440
+ this.body = buildResponseEnvelope({
2441
+ payload: data,
2442
+ wrap,
2443
+ rootKey,
2444
+ factory,
2445
+ context: {
2446
+ type: "resource",
2447
+ resource: this.resource
2448
+ }
2449
+ });
2450
+ this.body = appendRootProperties(this.body, customMeta, rootKey);
2451
+ this.body = this.applySerializePlugins(this.body);
2452
+ }
2453
+ return this;
2454
+ }
2455
+ /**
2456
+ * Convert resource to object format (for collections) or return original data for single resources.
2457
+ *
2458
+ * @returns
2459
+ */
2460
+ toObject() {
2461
+ this.called.toObject = true;
2462
+ this.json();
2463
+ let data = normalizeSerializableData(this.resource);
2464
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
2465
+ return data;
2466
+ }
2467
+ /**
2468
+ * Convert resource to object format and return original data.
2469
+ *
2470
+ * @deprecated Use toObject() instead.
2471
+ * @alias toArray
2472
+ * @since 0.2.9
2473
+ */
2474
+ toArray() {
2475
+ this.called.toArray = true;
2476
+ return this.toObject();
2477
+ }
2478
+ /**
2479
+ * Add additional properties to the response body
2480
+ *
2481
+ * @param extra Additional properties to merge into the response body
2482
+ * @returns
2483
+ */
2484
+ additional(extra) {
2485
+ this.called.additional = true;
2486
+ this.json();
2487
+ const payloadKey = this.getPayloadKey();
2488
+ if (extra.data && payloadKey && typeof this.body[payloadKey] !== "undefined") this.body[payloadKey] = Array.isArray(this.body[payloadKey]) ? [...this.body[payloadKey], ...extra.data] : {
2489
+ ...this.body[payloadKey],
2490
+ ...extra.data
2491
+ };
2492
+ this.body = {
2493
+ ...this.body,
2494
+ ...extra
2495
+ };
2496
+ return this;
2497
+ }
2498
+ /**
2499
+ * Build a response object, optionally accepting a raw response to mutate in withResponse.
2500
+ *
2501
+ * @param res Optional raw response object (e.g. Express Response or H3Event res)
2502
+ * @returns
2503
+ */
2504
+ response(res) {
2505
+ const rawResponse = this.resolveRawResponse(res ?? this.res);
2506
+ return this.runResponse({
2507
+ ensureJson: () => this.json(),
2508
+ rawResponse,
2509
+ body: () => this.body,
2510
+ createServerResponse: (raw, body) => {
2511
+ const response = new ServerResponse(raw, body);
2512
+ this.withResponseContext = {
2513
+ response,
2514
+ raw
2515
+ };
2516
+ return response;
2517
+ },
2518
+ callWithResponse: (response, raw) => {
2519
+ this.withResponse(response, raw);
2520
+ }
2521
+ });
2522
+ }
2523
+ /**
2524
+ * Customize the outgoing transport response right before dispatch.
2525
+ *
2526
+ * Override in custom classes to mutate headers/status/body.
2527
+ */
2528
+ withResponse(_response, _rawResponse) {
2529
+ return this;
2530
+ }
2531
+ /**
2532
+ * Promise-like then method to allow chaining with async/await or .then() syntax
2533
+ *
2534
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
2535
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
2536
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
2537
+ */
2538
+ then(onfulfilled, onrejected) {
2539
+ return this.runThen({
2540
+ ensureJson: () => this.json(),
2541
+ body: () => this.body,
2542
+ rawResponse: this.resolveRawResponse(this.res),
2543
+ createServerResponse: (raw, body) => {
2544
+ const response = new ServerResponse(raw, body);
2545
+ this.withResponseContext = {
2546
+ response,
2547
+ raw
2548
+ };
2549
+ return response;
2550
+ },
2551
+ callWithResponse: (response, raw) => {
2552
+ this.withResponse(response, raw);
2553
+ },
2554
+ sendRawResponse: (raw, body) => {
2555
+ this.sendRawResponseBody(raw, body);
2556
+ },
2557
+ onfulfilled,
2558
+ onrejected
2559
+ });
2560
+ }
2561
+ /**
2562
+ * Promise-like catch method to handle rejected state of the promise
2563
+ *
2564
+ * @param onrejected
2565
+ * @returns
2566
+ */
2567
+ catch(onrejected) {
2568
+ return this.runThen({
2569
+ ensureJson: () => this.json(),
2570
+ body: () => this.body,
2571
+ rawResponse: this.resolveRawResponse(this.res),
2572
+ createServerResponse: (raw, body) => {
2573
+ const response = new ServerResponse(raw, body);
2574
+ this.withResponseContext = {
2575
+ response,
2576
+ raw
2577
+ };
2578
+ return response;
2579
+ },
2580
+ callWithResponse: (response, raw) => {
2581
+ this.withResponse(response, raw);
2582
+ },
2583
+ sendRawResponse: (raw, body) => {
2584
+ this.sendRawResponseBody(raw, body);
2585
+ },
2586
+ onrejected
2587
+ });
2588
+ }
2589
+ /**
2590
+ * Promise-like finally method to handle cleanup after promise is settled
2591
+ *
2592
+ * @param onfinally
2593
+ * @returns
2594
+ */
2595
+ finally(onfinally) {
2596
+ return this.runThen({
2597
+ ensureJson: () => this.json(),
2598
+ body: () => this.body,
2599
+ rawResponse: this.resolveRawResponse(this.res),
2600
+ createServerResponse: (raw, body) => {
2601
+ const response = new ServerResponse(raw, body);
2602
+ this.withResponseContext = {
2603
+ response,
2604
+ raw
2605
+ };
2606
+ return response;
2607
+ },
2608
+ callWithResponse: (response, raw) => {
2609
+ this.withResponse(response, raw);
2610
+ },
2611
+ sendRawResponse: (raw, body) => {
2612
+ this.sendRawResponseBody(raw, body);
2613
+ },
2614
+ onfulfilled: onfinally,
2615
+ onrejected: onfinally
2616
+ });
2617
+ }
2618
+ };
2619
+
2620
+ //#endregion
2621
+ export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CliApp, GenericResource, InitCommand, MakeResource, Resource, ResourceCollection, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };