resora 0.2.15 → 0.2.18

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