resora 0.2.1 → 0.2.2
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/bin/index.mjs +5 -5
- package/dist/index.cjs +362 -5
- package/dist/index.d.cts +375 -4
- package/dist/index.d.mts +375 -4
- package/dist/index.mjs +358 -4
- package/package.json +1 -1
package/bin/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
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{Command as
|
|
2
|
+
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,Kernel as f}from"@h3ravel/musket";let p={first:`first`,last:`last`,prev:`prev`,next:`next`},m={to:`to`,from:`from`,links:`links`,path:`path`,total:`total`,per_page:`per_page`,last_page:`last_page`,current_page:`current_page`},h={previous:`previous`,next:`next`};const g=e=>{p={...p,...e}},_=e=>{m={...m,...e}},v=e=>{h={...h,...e}};let y=o.resolve(process.cwd(),`node_modules/resora/stubs`);t(y)||(y=o.resolve(process.cwd(),`stubs`));const b=()=>({stubsDir:y,preferredCase:`camel`,responseStructure:{wrap:!0,rootKey:`data`},paginatedExtras:[`meta`,`links`],baseUrl:`https://localhost`,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`}}),x=e=>{let t=b();return Object.assign(t,e,{stubs:Object.assign(t.stubs,e.stubs||{})},{cursorMeta:Object.assign(t.cursorMeta,e.cursorMeta||{})},{paginatedMeta:Object.assign(t.paginatedMeta,e.paginatedMeta||{})},{paginatedLinks:Object.assign(t.paginatedLinks,e.paginatedLinks||{})},{responseStructure:Object.assign(t.responseStructure,e.responseStructure||{})})};let S=!1,C;const w=e=>{e.preferredCase!==`camel`&&e.preferredCase,e.responseStructure,e.paginatedExtras,g(e.paginatedLinks),_(e.paginatedMeta),v(e.cursorMeta),e.baseUrl,e.pageName},T=async e=>await import(`${u(e).href}?resora_runtime=${Date.now()}`),E=e=>{w(x((e?.default??e)||{})),S=!0},D=()=>{let e=l(import.meta.url),n=[o.join(process.cwd(),`resora.config.cjs`)];for(let r of n)if(t(r))try{return E(e(r)),!0}catch{continue}return!1};(async()=>{if(!S){if(C)return await C;D()||(C=(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{E(await T(n));return}catch{continue}S=!0})(),await C)}})();var O=class{command;config={};constructor(e={}){this.config=x(e)}async loadConfig(e={}){this.config=x(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));let s=r.replace(/(Resource|Collection)$/,``)+`Resource`,l=`/**
|
|
3
3
|
* The resource that this collection collects.
|
|
4
4
|
*/
|
|
5
5
|
collects = ${s}
|
|
6
|
-
`,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}}},
|
|
6
|
+
`,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}}},k=class extends d{signature=`init
|
|
7
7
|
{--force : Force overwrite if config file already exists (existing file will be backed up) }
|
|
8
|
-
`;description=`Initialize Resora`;async handle(){this.app.command=this,this.app.init(),this.success(`Resora initialized`)}},
|
|
8
|
+
`;description=`Initialize Resora`;async handle(){this.app.command=this,this.app.init(),this.success(`Resora initialized`)}},A=class extends d{signature=`#create:
|
|
9
9
|
{resource : Generates a new resource file.
|
|
10
10
|
| {name : Name of the resource to create}
|
|
11
11
|
| {--c|collection : Make a resource collection}
|
|
@@ -19,11 +19,11 @@ import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,rmSync
|
|
|
19
19
|
| {prefix : prefix of the resources to create, "Admin" will create AdminResource, AdminCollection}
|
|
20
20
|
| {--force : Create the resource or collection file even if it already exists.}
|
|
21
21
|
}
|
|
22
|
-
`;description=`Create a new resource or resource collection file`;async handle(){this.app.command=this;let e=``,t=this.dictionary.name||this.dictionary.baseCommand;if([`resource`,`collection`].includes(t)&&!this.argument(`name`))return void this.error(`Error: Name argument is required.`);if(t===`all`&&!this.argument(`prefix`))return void this.error(`Error: Prefix argument is required.`);switch(t){case`resource`:({path:e}=this.app.makeResource(this.argument(`name`),this.options()));break;case`collection`:({path:e}=this.app.makeResource(this.argument(`name`)+`Collection`,this.options()));break;case`all`:{let t=this.app.makeResource(this.argument(`prefix`),{force:this.option(`force`)}),n=this.app.makeResource(this.argument(`prefix`)+`Collection`,{collection:!0,force:this.option(`force`)});e=`${t.path}, ${n.path}`;break}default:this.fail(`Unknown action: ${t}`)}this.success(`Created: ${e}`)}},
|
|
22
|
+
`;description=`Create a new resource or resource collection file`;async handle(){this.app.command=this;let e=``,t=this.dictionary.name||this.dictionary.baseCommand;if([`resource`,`collection`].includes(t)&&!this.argument(`name`))return void this.error(`Error: Name argument is required.`);if(t===`all`&&!this.argument(`prefix`))return void this.error(`Error: Prefix argument is required.`);switch(t){case`resource`:({path:e}=this.app.makeResource(this.argument(`name`),this.options()));break;case`collection`:({path:e}=this.app.makeResource(this.argument(`name`)+`Collection`,this.options()));break;case`all`:{let t=this.app.makeResource(this.argument(`prefix`),{force:this.option(`force`)}),n=this.app.makeResource(this.argument(`prefix`)+`Collection`,{collection:!0,force:this.option(`force`)});e=`${t.path}, ${n.path}`;break}default:this.fail(`Unknown action: ${t}`)}this.success(`Created: ${e}`)}},j=String.raw`
|
|
23
23
|
_____
|
|
24
24
|
| __ \
|
|
25
25
|
| |__) |___ ___ ___ _ __ __ _
|
|
26
26
|
| _ // _ \/ __|/ _ \| '__/ _, |
|
|
27
27
|
| | \ \ __/\__ \ (_) | | | (_| |
|
|
28
28
|
|_| \_\___||___/\___/|_| \__,_|
|
|
29
|
-
`;const
|
|
29
|
+
`;const M=new O;await f.init(await M.loadConfig(),{logo:j,name:`Resora CLI`,baseCommands:[A,k,...M.getConfig().extraCommands||[]],exceptionHandler(e){throw e}});export{};
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
let fs = require("fs");
|
|
30
30
|
let path = require("path");
|
|
31
31
|
path = __toESM(path);
|
|
32
|
+
let module$1 = require("module");
|
|
33
|
+
let url = require("url");
|
|
32
34
|
let _h3ravel_musket = require("@h3ravel/musket");
|
|
33
35
|
|
|
34
36
|
//#region src/ApiResource.ts
|
|
@@ -69,93 +71,227 @@ let globalCursorMeta = {
|
|
|
69
71
|
previous: "previous",
|
|
70
72
|
next: "next"
|
|
71
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Sets the global case style for response keys, which will be applied
|
|
76
|
+
* to all responses unless overridden by individual resource configurations.
|
|
77
|
+
*
|
|
78
|
+
* @param style The case style to set as the global default for response keys.
|
|
79
|
+
*/
|
|
72
80
|
const setGlobalCase = (style) => {
|
|
73
81
|
globalPreferredCase = style;
|
|
74
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Retrieves the global case style for response keys, which is used
|
|
85
|
+
* to determine how keys in responses should be formatted.
|
|
86
|
+
*
|
|
87
|
+
* @returns
|
|
88
|
+
*/
|
|
75
89
|
const getGlobalCase = () => {
|
|
76
90
|
return globalPreferredCase;
|
|
77
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* Sets the global response structure configuration, which defines how
|
|
94
|
+
* responses should be structured across the application.
|
|
95
|
+
*
|
|
96
|
+
* @param config The response structure configuration object.
|
|
97
|
+
*/
|
|
78
98
|
const setGlobalResponseStructure = (config) => {
|
|
79
99
|
globalResponseStructure = config;
|
|
80
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Retrieves the global response structure configuration, which
|
|
103
|
+
* defines how responses should be structured across the application.
|
|
104
|
+
*
|
|
105
|
+
* @returns
|
|
106
|
+
*/
|
|
81
107
|
const getGlobalResponseStructure = () => {
|
|
82
108
|
return globalResponseStructure;
|
|
83
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Sets the global response root key, which is the key under which
|
|
112
|
+
* the main data will be nested in responses if wrapping is enabled.
|
|
113
|
+
*
|
|
114
|
+
* @param rootKey The root key to set for response data.
|
|
115
|
+
*/
|
|
84
116
|
const setGlobalResponseRootKey = (rootKey) => {
|
|
85
117
|
globalResponseStructure = {
|
|
86
118
|
...globalResponseStructure || {},
|
|
87
119
|
rootKey
|
|
88
120
|
};
|
|
89
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Sets the global response wrap option, which determines whether responses
|
|
124
|
+
* should be wrapped in a root key or returned unwrapped when possible.
|
|
125
|
+
*
|
|
126
|
+
* @param wrap The wrap option to set for responses.
|
|
127
|
+
*/
|
|
90
128
|
const setGlobalResponseWrap = (wrap) => {
|
|
91
129
|
globalResponseStructure = {
|
|
92
130
|
...globalResponseStructure || {},
|
|
93
131
|
wrap
|
|
94
132
|
};
|
|
95
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Retrieves the global response wrap option, which indicates whether responses
|
|
136
|
+
* should be wrapped in a root key or returned unwrapped when possible.
|
|
137
|
+
*
|
|
138
|
+
* @returns
|
|
139
|
+
*/
|
|
96
140
|
const getGlobalResponseWrap = () => {
|
|
97
141
|
return globalResponseStructure?.wrap;
|
|
98
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* Retrieves the global response root key, which is the key under which the main
|
|
145
|
+
* data will be nested in responses if wrapping is enabled.
|
|
146
|
+
*
|
|
147
|
+
* @returns
|
|
148
|
+
*/
|
|
99
149
|
const getGlobalResponseRootKey = () => {
|
|
100
150
|
return globalResponseStructure?.rootKey;
|
|
101
151
|
};
|
|
152
|
+
/**
|
|
153
|
+
* Sets the global response factory, which is a custom function that can be used
|
|
154
|
+
* to produce a completely custom response structure based on the provided
|
|
155
|
+
* payload and context.
|
|
156
|
+
*
|
|
157
|
+
* @param factory The response factory function to set as the global default for response construction.
|
|
158
|
+
*/
|
|
102
159
|
const setGlobalResponseFactory = (factory) => {
|
|
103
160
|
globalResponseStructure = {
|
|
104
161
|
...globalResponseStructure || {},
|
|
105
162
|
factory
|
|
106
163
|
};
|
|
107
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
* Retrieves the global response factory, which is a custom function that
|
|
167
|
+
* can be used to produce a completely custom response structure based on
|
|
168
|
+
* the provided payload and context.
|
|
169
|
+
*
|
|
170
|
+
* @returns
|
|
171
|
+
*/
|
|
108
172
|
const getGlobalResponseFactory = () => {
|
|
109
173
|
return globalResponseStructure?.factory;
|
|
110
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Sets the global paginated extras configuration, which defines the keys
|
|
177
|
+
* to use for pagination metadata, links, and cursor information in paginated responses.
|
|
178
|
+
*
|
|
179
|
+
* @param extras The paginated extras configuration object.
|
|
180
|
+
*/
|
|
111
181
|
const setGlobalPaginatedExtras = (extras) => {
|
|
112
182
|
globalPaginatedExtras = extras;
|
|
113
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* Retrieves the global paginated extras configuration, which defines the keys to use for pagination metadata, links, and cursor information in paginated responses.
|
|
186
|
+
*
|
|
187
|
+
* @returns
|
|
188
|
+
*/
|
|
114
189
|
const getGlobalPaginatedExtras = () => {
|
|
115
190
|
return globalPaginatedExtras;
|
|
116
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* Sets the global paginated links configuration, which defines the keys to
|
|
194
|
+
* use for pagination links (first, last, prev, next) in paginated responses.
|
|
195
|
+
*
|
|
196
|
+
* @param links The paginated links configuration object.
|
|
197
|
+
*/
|
|
117
198
|
const setGlobalPaginatedLinks = (links) => {
|
|
118
199
|
globalPaginatedLinks = {
|
|
119
200
|
...globalPaginatedLinks,
|
|
120
201
|
...links
|
|
121
202
|
};
|
|
122
203
|
};
|
|
204
|
+
/**
|
|
205
|
+
* Retrieves the global paginated links configuration, which defines the keys to use for pagination links (first, last, prev, next) in paginated responses.
|
|
206
|
+
*
|
|
207
|
+
* @returns
|
|
208
|
+
*/
|
|
123
209
|
const getGlobalPaginatedLinks = () => {
|
|
124
210
|
return globalPaginatedLinks;
|
|
125
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Sets the global base URL, which is used for generating pagination links in responses.
|
|
214
|
+
*
|
|
215
|
+
* @param baseUrl The base URL to set for pagination link generation.
|
|
216
|
+
*/
|
|
126
217
|
const setGlobalBaseUrl = (baseUrl) => {
|
|
127
218
|
globalBaseUrl = baseUrl;
|
|
128
219
|
};
|
|
220
|
+
/**
|
|
221
|
+
* Retrieves the global base URL, which is used for generating pagination links in responses.
|
|
222
|
+
*
|
|
223
|
+
* @returns
|
|
224
|
+
*/
|
|
129
225
|
const getGlobalBaseUrl = () => {
|
|
130
226
|
return globalBaseUrl;
|
|
131
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* Sets the global page name, which is the query parameter name used for the page number in paginated requests and link generation.
|
|
230
|
+
*
|
|
231
|
+
* @param pageName
|
|
232
|
+
*/
|
|
132
233
|
const setGlobalPageName = (pageName) => {
|
|
133
234
|
globalPageName = pageName;
|
|
134
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* Retrieves the global page name, which is the query parameter name
|
|
238
|
+
* used for the page number in paginated requests and link generation.
|
|
239
|
+
*
|
|
240
|
+
* @returns
|
|
241
|
+
*/
|
|
135
242
|
const getGlobalPageName = () => {
|
|
136
243
|
return globalPageName;
|
|
137
244
|
};
|
|
245
|
+
/**
|
|
246
|
+
* Retrieves the keys to use for pagination extras (meta, links, cursor) based
|
|
247
|
+
* on the global configuration.
|
|
248
|
+
*
|
|
249
|
+
* @param meta Whether to include pagination metadata in the response.
|
|
250
|
+
*/
|
|
138
251
|
const setGlobalPaginatedMeta = (meta) => {
|
|
139
252
|
globalPaginatedMeta = {
|
|
140
253
|
...globalPaginatedMeta,
|
|
141
254
|
...meta
|
|
142
255
|
};
|
|
143
256
|
};
|
|
257
|
+
/**
|
|
258
|
+
* Retrieves the keys to use for pagination extras (meta, links, cursor) based
|
|
259
|
+
* on the global configuration.
|
|
260
|
+
*
|
|
261
|
+
* @returns The global pagination metadata configuration.
|
|
262
|
+
*/
|
|
144
263
|
const getGlobalPaginatedMeta = () => {
|
|
145
264
|
return globalPaginatedMeta;
|
|
146
265
|
};
|
|
266
|
+
/**
|
|
267
|
+
* Sets the global cursor meta configuration, which defines the keys to use
|
|
268
|
+
* for cursor pagination metadata (previous, next) in responses.
|
|
269
|
+
*
|
|
270
|
+
* @param meta The cursor meta configuration object.
|
|
271
|
+
*/
|
|
147
272
|
const setGlobalCursorMeta = (meta) => {
|
|
148
273
|
globalCursorMeta = {
|
|
149
274
|
...globalCursorMeta,
|
|
150
275
|
...meta
|
|
151
276
|
};
|
|
152
277
|
};
|
|
278
|
+
/**
|
|
279
|
+
* Retrieves the keys to use for cursor pagination metadata (previous, next) in
|
|
280
|
+
* responses based on the global configuration.
|
|
281
|
+
*
|
|
282
|
+
* @returns The global cursor pagination metadata configuration.
|
|
283
|
+
*/
|
|
153
284
|
const getGlobalCursorMeta = () => {
|
|
154
285
|
return globalCursorMeta;
|
|
155
286
|
};
|
|
156
287
|
|
|
157
288
|
//#endregion
|
|
158
289
|
//#region src/utilities/pagination.ts
|
|
290
|
+
/**
|
|
291
|
+
* Retrieves the configured keys for pagination extras (meta, links, cursor) based on the application's configuration.
|
|
292
|
+
*
|
|
293
|
+
* @returns An object containing the keys for meta, links, and cursor extras, or `undefined` if not configured.
|
|
294
|
+
*/
|
|
159
295
|
const getPaginationExtraKeys = () => {
|
|
160
296
|
const extras = getGlobalPaginatedExtras();
|
|
161
297
|
if (Array.isArray(extras)) return {
|
|
@@ -169,6 +305,13 @@ const getPaginationExtraKeys = () => {
|
|
|
169
305
|
cursorKey: extras.cursor
|
|
170
306
|
};
|
|
171
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* Builds a pagination URL for a given page number and path, using the global base URL and page name configuration.
|
|
310
|
+
*
|
|
311
|
+
* @param page The page number for which to build the URL. If `undefined`, the function will return `undefined`.
|
|
312
|
+
* @param pathName The path to use for the URL. If not provided, it will default to an empty string.
|
|
313
|
+
* @returns
|
|
314
|
+
*/
|
|
172
315
|
const buildPageUrl = (page, pathName) => {
|
|
173
316
|
if (typeof page === "undefined") return;
|
|
174
317
|
const rawPath = pathName || "";
|
|
@@ -182,6 +325,13 @@ const buildPageUrl = (page, pathName) => {
|
|
|
182
325
|
url.searchParams.set(getGlobalPageName() || "page", String(page));
|
|
183
326
|
return url.toString();
|
|
184
327
|
};
|
|
328
|
+
/**
|
|
329
|
+
* Builds pagination extras (meta, links, cursor) for a given resource based on
|
|
330
|
+
* its pagination and cursor properties, using the configured keys for each type of extra.
|
|
331
|
+
*
|
|
332
|
+
* @param resource The resource for which to build pagination extras.
|
|
333
|
+
* @returns An object containing the pagination extras (meta, links, cursor) for the resource.
|
|
334
|
+
*/
|
|
185
335
|
const buildPaginationExtras = (resource) => {
|
|
186
336
|
const { metaKey, linksKey, cursorKey } = getPaginationExtraKeys();
|
|
187
337
|
const extra = {};
|
|
@@ -238,12 +388,26 @@ const buildPaginationExtras = (resource) => {
|
|
|
238
388
|
|
|
239
389
|
//#endregion
|
|
240
390
|
//#region src/utilities/objects.ts
|
|
391
|
+
/**
|
|
392
|
+
* Utility functions for working with objects, including type checking, merging, and property manipulation.
|
|
393
|
+
*
|
|
394
|
+
* @param value The value to check.
|
|
395
|
+
* @returns `true` if the value is a plain object, `false` otherwise.
|
|
396
|
+
*/
|
|
241
397
|
const isPlainObject = (value) => {
|
|
242
398
|
if (typeof value !== "object" || value === null) return false;
|
|
243
399
|
if (Array.isArray(value) || value instanceof Date || value instanceof RegExp) return false;
|
|
244
400
|
const proto = Object.getPrototypeOf(value);
|
|
245
401
|
return proto === Object.prototype || proto === null;
|
|
246
402
|
};
|
|
403
|
+
/**
|
|
404
|
+
* Appends extra properties to a response body, ensuring that the main data is wrapped under a specified root key if necessary.
|
|
405
|
+
*
|
|
406
|
+
* @param body The original response body, which can be an object, array, or primitive value.
|
|
407
|
+
* @param extra Extra properties to append to the response body.
|
|
408
|
+
* @param rootKey The root key under which to wrap the main data if necessary.
|
|
409
|
+
* @returns The response body with the extra properties appended.
|
|
410
|
+
*/
|
|
247
411
|
const appendRootProperties = (body, extra, rootKey = "data") => {
|
|
248
412
|
if (!extra || Object.keys(extra).length === 0) return body;
|
|
249
413
|
if (Array.isArray(body)) return {
|
|
@@ -259,6 +423,13 @@ const appendRootProperties = (body, extra, rootKey = "data") => {
|
|
|
259
423
|
...extra
|
|
260
424
|
};
|
|
261
425
|
};
|
|
426
|
+
/**
|
|
427
|
+
* Deeply merges two metadata objects, combining nested objects recursively.
|
|
428
|
+
*
|
|
429
|
+
* @param base The base metadata object to merge into.
|
|
430
|
+
* @param incoming The incoming metadata object to merge from.
|
|
431
|
+
* @returns
|
|
432
|
+
*/
|
|
262
433
|
const mergeMetadata = (base, incoming) => {
|
|
263
434
|
if (!incoming) return base;
|
|
264
435
|
if (!base) return incoming;
|
|
@@ -273,6 +444,12 @@ const mergeMetadata = (base, incoming) => {
|
|
|
273
444
|
|
|
274
445
|
//#endregion
|
|
275
446
|
//#region src/utilities/response.ts
|
|
447
|
+
/**
|
|
448
|
+
* Builds a response envelope based on the provided payload,
|
|
449
|
+
* metadata, and configuration options.
|
|
450
|
+
*
|
|
451
|
+
* @param config The configuration object containing payload, metadata, and other options for building the response.
|
|
452
|
+
*/
|
|
276
453
|
const buildResponseEnvelope = ({ payload, meta, metaKey = "meta", wrap = true, rootKey = "data", factory, context }) => {
|
|
277
454
|
if (factory) return factory(payload, {
|
|
278
455
|
...context,
|
|
@@ -297,6 +474,15 @@ const buildResponseEnvelope = ({ payload, meta, metaKey = "meta", wrap = true, r
|
|
|
297
474
|
|
|
298
475
|
//#endregion
|
|
299
476
|
//#region src/utilities/metadata.ts
|
|
477
|
+
/**
|
|
478
|
+
* Resolves metadata from a resource instance by checking for a custom `with` method.
|
|
479
|
+
* If the method exists and is different from the base method, it calls it to retrieve metadata.
|
|
480
|
+
* This allows resources to provide additional metadata for response construction.
|
|
481
|
+
*
|
|
482
|
+
* @param instance The resource instance to check for a custom `with` method.
|
|
483
|
+
* @param baseWithMethod The base `with` method to compare against.
|
|
484
|
+
* @returns The resolved metadata or `undefined` if no custom metadata is provided.
|
|
485
|
+
*/
|
|
300
486
|
const resolveWithHookMetadata = (instance, baseWithMethod) => {
|
|
301
487
|
const candidate = instance?.with;
|
|
302
488
|
if (typeof candidate !== "function" || candidate === baseWithMethod) return;
|
|
@@ -308,18 +494,45 @@ const resolveWithHookMetadata = (instance, baseWithMethod) => {
|
|
|
308
494
|
//#endregion
|
|
309
495
|
//#region src/utilities/conditional.ts
|
|
310
496
|
const CONDITIONAL_ATTRIBUTE_MISSING = Symbol("resora.conditional.missing");
|
|
497
|
+
/**
|
|
498
|
+
* Resolves a value based on a condition. If the condition is falsy, it returns a special symbol indicating that the attribute is missing.
|
|
499
|
+
*
|
|
500
|
+
*
|
|
501
|
+
* @param condition The condition to evaluate.
|
|
502
|
+
* @param value The value or function to resolve if the condition is truthy.
|
|
503
|
+
* @returns The resolved value or a symbol indicating the attribute is missing.
|
|
504
|
+
*/
|
|
311
505
|
const resolveWhen = (condition, value) => {
|
|
312
506
|
if (!condition) return CONDITIONAL_ATTRIBUTE_MISSING;
|
|
313
507
|
return typeof value === "function" ? value() : value;
|
|
314
508
|
};
|
|
509
|
+
/**
|
|
510
|
+
* Resolves a value only if it is not null or undefined.
|
|
511
|
+
*
|
|
512
|
+
* @param value The value to resolve.
|
|
513
|
+
* @returns The resolved value or a symbol indicating the attribute is missing.
|
|
514
|
+
*/
|
|
315
515
|
const resolveWhenNotNull = (value) => {
|
|
316
516
|
return value === null || typeof value === "undefined" ? CONDITIONAL_ATTRIBUTE_MISSING : value;
|
|
317
517
|
};
|
|
518
|
+
/**
|
|
519
|
+
* Conditionally merges object attributes based on a condition.
|
|
520
|
+
*
|
|
521
|
+
* @param condition
|
|
522
|
+
* @param value
|
|
523
|
+
* @returns
|
|
524
|
+
*/
|
|
318
525
|
const resolveMergeWhen = (condition, value) => {
|
|
319
526
|
if (!condition) return {};
|
|
320
527
|
const resolved = typeof value === "function" ? value() : value;
|
|
321
528
|
return isPlainObject(resolved) ? resolved : {};
|
|
322
529
|
};
|
|
530
|
+
/**
|
|
531
|
+
* Recursively sanitizes an object or array by removing any attributes that are marked as missing using the special symbol.
|
|
532
|
+
*
|
|
533
|
+
* @param value The value to sanitize.
|
|
534
|
+
* @returns The sanitized value.
|
|
535
|
+
*/
|
|
323
536
|
const sanitizeConditionalAttributes = (value) => {
|
|
324
537
|
if (value === CONDITIONAL_ATTRIBUTE_MISSING) return CONDITIONAL_ATTRIBUTE_MISSING;
|
|
325
538
|
if (Array.isArray(value)) return value.map((item) => sanitizeConditionalAttributes(item)).filter((item) => item !== CONDITIONAL_ATTRIBUTE_MISSING);
|
|
@@ -337,21 +550,58 @@ const sanitizeConditionalAttributes = (value) => {
|
|
|
337
550
|
|
|
338
551
|
//#endregion
|
|
339
552
|
//#region src/utilities/case.ts
|
|
553
|
+
/**
|
|
554
|
+
* Splits a string into words based on common delimiters and capitalization patterns.
|
|
555
|
+
* Handles camelCase, PascalCase, snake_case, kebab-case, and space-separated words.
|
|
556
|
+
*
|
|
557
|
+
* @param str
|
|
558
|
+
* @returns
|
|
559
|
+
*/
|
|
340
560
|
const splitWords = (str) => {
|
|
341
561
|
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);
|
|
342
562
|
};
|
|
563
|
+
/**
|
|
564
|
+
* Transforms a string to camelCase.
|
|
565
|
+
*
|
|
566
|
+
* @param str
|
|
567
|
+
* @returns
|
|
568
|
+
*/
|
|
343
569
|
const toCamelCase = (str) => {
|
|
344
570
|
return splitWords(str).map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
345
571
|
};
|
|
572
|
+
/**
|
|
573
|
+
* Transforms a string to snake_case.
|
|
574
|
+
*
|
|
575
|
+
* @param str
|
|
576
|
+
* @returns
|
|
577
|
+
*/
|
|
346
578
|
const toSnakeCase = (str) => {
|
|
347
579
|
return splitWords(str).join("_");
|
|
348
580
|
};
|
|
581
|
+
/**
|
|
582
|
+
* Transforms a string to PascalCase.
|
|
583
|
+
*
|
|
584
|
+
* @param str
|
|
585
|
+
* @returns
|
|
586
|
+
*/
|
|
349
587
|
const toPascalCase = (str) => {
|
|
350
588
|
return splitWords(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
351
589
|
};
|
|
590
|
+
/**
|
|
591
|
+
* Transforms a string to kebab-case.
|
|
592
|
+
*
|
|
593
|
+
* @param str
|
|
594
|
+
* @returns
|
|
595
|
+
*/
|
|
352
596
|
const toKebabCase = (str) => {
|
|
353
597
|
return splitWords(str).join("-");
|
|
354
598
|
};
|
|
599
|
+
/**
|
|
600
|
+
* Retrieves the appropriate case transformation function based on the provided style.
|
|
601
|
+
*
|
|
602
|
+
* @param style
|
|
603
|
+
* @returns
|
|
604
|
+
*/
|
|
355
605
|
const getCaseTransformer = (style) => {
|
|
356
606
|
if (typeof style === "function") return style;
|
|
357
607
|
switch (style) {
|
|
@@ -361,6 +611,14 @@ const getCaseTransformer = (style) => {
|
|
|
361
611
|
case "kebab": return toKebabCase;
|
|
362
612
|
}
|
|
363
613
|
};
|
|
614
|
+
/**
|
|
615
|
+
* Transforms the keys of an object (including nested objects and arrays) using
|
|
616
|
+
* the provided transformer function.
|
|
617
|
+
*
|
|
618
|
+
* @param obj
|
|
619
|
+
* @param transformer
|
|
620
|
+
* @returns
|
|
621
|
+
*/
|
|
364
622
|
const transformKeys = (obj, transformer) => {
|
|
365
623
|
if (obj === null || obj === void 0) return obj;
|
|
366
624
|
if (Array.isArray(obj)) return obj.map((item) => transformKeys(item, transformer));
|
|
@@ -412,10 +670,106 @@ const getDefaultConfig = () => {
|
|
|
412
670
|
}
|
|
413
671
|
};
|
|
414
672
|
};
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
673
|
+
/**
|
|
674
|
+
* 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.
|
|
675
|
+
*
|
|
676
|
+
* @param config
|
|
677
|
+
* @returns
|
|
678
|
+
*/
|
|
679
|
+
const defineConfig = (config) => {
|
|
680
|
+
const defConf = getDefaultConfig();
|
|
681
|
+
return Object.assign(defConf, config, { stubs: Object.assign(defConf.stubs, config.stubs || {}) }, { cursorMeta: Object.assign(defConf.cursorMeta, config.cursorMeta || {}) }, { paginatedMeta: Object.assign(defConf.paginatedMeta, config.paginatedMeta || {}) }, { paginatedLinks: Object.assign(defConf.paginatedLinks, config.paginatedLinks || {}) }, { responseStructure: Object.assign(defConf.responseStructure, config.responseStructure || {}) });
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/utilities/runtime-config.ts
|
|
686
|
+
let runtimeConfigLoaded = false;
|
|
687
|
+
let runtimeConfigLoadingPromise;
|
|
688
|
+
/**
|
|
689
|
+
* Resets the runtime configuration state for testing purposes.
|
|
690
|
+
*
|
|
691
|
+
* @returns
|
|
692
|
+
*/
|
|
693
|
+
const resetRuntimeConfigForTests = () => {
|
|
694
|
+
runtimeConfigLoaded = false;
|
|
695
|
+
runtimeConfigLoadingPromise = void 0;
|
|
696
|
+
};
|
|
697
|
+
/**
|
|
698
|
+
* Applies the provided configuration to the global state of the application.
|
|
699
|
+
*
|
|
700
|
+
* @param config The complete configuration object to apply.
|
|
701
|
+
*/
|
|
702
|
+
const applyRuntimeConfig = (config) => {
|
|
703
|
+
if (config.preferredCase !== "camel") setGlobalCase(config.preferredCase);
|
|
704
|
+
setGlobalResponseStructure(config.responseStructure);
|
|
705
|
+
setGlobalPaginatedExtras(config.paginatedExtras);
|
|
706
|
+
setGlobalPaginatedLinks(config.paginatedLinks);
|
|
707
|
+
setGlobalPaginatedMeta(config.paginatedMeta);
|
|
708
|
+
setGlobalCursorMeta(config.cursorMeta);
|
|
709
|
+
setGlobalBaseUrl(config.baseUrl);
|
|
710
|
+
setGlobalPageName(config.pageName);
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Loads the runtime configuration by searching for configuration files in the current working directory.
|
|
714
|
+
* @param configPath The path to the configuration file to load.
|
|
715
|
+
* @returns
|
|
716
|
+
*/
|
|
717
|
+
const importConfigFile = async (configPath) => {
|
|
718
|
+
return await import(`${(0, url.pathToFileURL)(configPath).href}?resora_runtime=${Date.now()}`);
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Resolves the imported configuration and applies it to the global state.
|
|
722
|
+
*
|
|
723
|
+
* @param imported
|
|
724
|
+
*/
|
|
725
|
+
const resolveAndApply = (imported) => {
|
|
726
|
+
applyRuntimeConfig(defineConfig((imported?.default ?? imported) || {}));
|
|
727
|
+
runtimeConfigLoaded = true;
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* Loads the runtime configuration synchronously by searching for CommonJS configuration files in the current working directory.
|
|
731
|
+
*
|
|
732
|
+
* @returns
|
|
733
|
+
*/
|
|
734
|
+
const loadRuntimeConfigSync = () => {
|
|
735
|
+
const require = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
736
|
+
const syncConfigPaths = [path.default.join(process.cwd(), "resora.config.cjs")];
|
|
737
|
+
for (const configPath of syncConfigPaths) {
|
|
738
|
+
if (!(0, fs.existsSync)(configPath)) continue;
|
|
739
|
+
try {
|
|
740
|
+
resolveAndApply(require(configPath));
|
|
741
|
+
return true;
|
|
742
|
+
} catch {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
};
|
|
748
|
+
/**
|
|
749
|
+
* Loads the runtime configuration by searching for configuration files in the current working directory.
|
|
750
|
+
*
|
|
751
|
+
* @returns
|
|
752
|
+
*/
|
|
753
|
+
const loadRuntimeConfig = async () => {
|
|
754
|
+
if (runtimeConfigLoaded) return;
|
|
755
|
+
if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
|
|
756
|
+
if (loadRuntimeConfigSync()) return;
|
|
757
|
+
runtimeConfigLoadingPromise = (async () => {
|
|
758
|
+
const possibleConfigPaths = [path.default.join(process.cwd(), "resora.config.js"), path.default.join(process.cwd(), "resora.config.ts")];
|
|
759
|
+
for (const configPath of possibleConfigPaths) {
|
|
760
|
+
if (!(0, fs.existsSync)(configPath)) continue;
|
|
761
|
+
try {
|
|
762
|
+
resolveAndApply(await importConfigFile(configPath));
|
|
763
|
+
return;
|
|
764
|
+
} catch {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
runtimeConfigLoaded = true;
|
|
769
|
+
})();
|
|
770
|
+
await runtimeConfigLoadingPromise;
|
|
418
771
|
};
|
|
772
|
+
loadRuntimeConfig();
|
|
419
773
|
|
|
420
774
|
//#endregion
|
|
421
775
|
//#region src/cli/CliApp.ts
|
|
@@ -517,7 +871,7 @@ var CliApp = class {
|
|
|
517
871
|
`;
|
|
518
872
|
const collectsImport = `import ${collectsName} from './${collectsName}'\n`;
|
|
519
873
|
const hasCollects = (!!options?.collection || name.endsWith("Collection")) && (0, fs.existsSync)((0, path.join)(this.config.resourcesDir, `${collectsName}.ts`));
|
|
520
|
-
const path$
|
|
874
|
+
const path$3 = this.generateFile(stubPath, outputPath, {
|
|
521
875
|
ResourceName: resourceName,
|
|
522
876
|
CollectionResourceName: resourceName.replace(/(Resource|Collection)$/, "") + "Resource",
|
|
523
877
|
"collects = Resource": hasCollects ? collects : "",
|
|
@@ -525,7 +879,7 @@ var CliApp = class {
|
|
|
525
879
|
}, options);
|
|
526
880
|
return {
|
|
527
881
|
name: resourceName,
|
|
528
|
-
path: path$
|
|
882
|
+
path: path$3
|
|
529
883
|
};
|
|
530
884
|
}
|
|
531
885
|
};
|
|
@@ -1483,6 +1837,7 @@ exports.Resource = Resource;
|
|
|
1483
1837
|
exports.ResourceCollection = ResourceCollection;
|
|
1484
1838
|
exports.ServerResponse = ServerResponse;
|
|
1485
1839
|
exports.appendRootProperties = appendRootProperties;
|
|
1840
|
+
exports.applyRuntimeConfig = applyRuntimeConfig;
|
|
1486
1841
|
exports.buildPaginationExtras = buildPaginationExtras;
|
|
1487
1842
|
exports.buildResponseEnvelope = buildResponseEnvelope;
|
|
1488
1843
|
exports.defineConfig = defineConfig;
|
|
@@ -1501,7 +1856,9 @@ exports.getGlobalResponseStructure = getGlobalResponseStructure;
|
|
|
1501
1856
|
exports.getGlobalResponseWrap = getGlobalResponseWrap;
|
|
1502
1857
|
exports.getPaginationExtraKeys = getPaginationExtraKeys;
|
|
1503
1858
|
exports.isPlainObject = isPlainObject;
|
|
1859
|
+
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
1504
1860
|
exports.mergeMetadata = mergeMetadata;
|
|
1861
|
+
exports.resetRuntimeConfigForTests = resetRuntimeConfigForTests;
|
|
1505
1862
|
exports.resolveMergeWhen = resolveMergeWhen;
|
|
1506
1863
|
exports.resolveWhen = resolveWhen;
|
|
1507
1864
|
exports.resolveWhenNotNull = resolveWhenNotNull;
|