resora 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -10
- package/bin/index.mjs +2 -2
- package/dist/index.cjs +831 -29
- package/dist/index.d.cts +346 -158
- package/dist/index.d.mts +346 -158
- package/dist/index.mjs +789 -29
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/resora)
|
|
4
4
|
[](https://www.npmjs.com/package/resora)
|
|
5
|
-
[](https://github.com/
|
|
6
|
-
[](https://github.com/arcstack-hq/resora/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/arcstack-hq/resora/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/arcstack-hq/resora/actions/workflows/deploy-docs.yml)
|
|
8
8
|
|
|
9
9
|
Resora is a structured API response layer for Node.js and TypeScript backends.
|
|
10
10
|
|
|
11
|
-
It provides a clean, explicit way to transform data into consistent JSON responses and automatically send them to the client. Resora supports single resources, collections, and
|
|
11
|
+
It provides a clean, explicit way to transform data into consistent JSON responses and automatically send them to the client. Resora supports single resources, collections, pagination and cursor metadata, conditional attributes, and response customization while remaining framework-agnostic and strongly typed.
|
|
12
12
|
|
|
13
13
|
Resora is designed for teams that care about long-term maintainability, predictable API contracts, and clean separation of concerns.
|
|
14
14
|
|
|
@@ -33,6 +33,10 @@ Resora introduces a dedicated **response transformation layer** that removes the
|
|
|
33
33
|
- Automatic JSON response dispatch
|
|
34
34
|
- First-class collection support
|
|
35
35
|
- Built-in pagination metadata handling
|
|
36
|
+
- Built-in cursor metadata handling
|
|
37
|
+
- Conditional attribute helpers (`when`, `whenNotNull`, `mergeWhen`)
|
|
38
|
+
- Configurable response envelope (`wrap`, `rootKey`, `factory`)
|
|
39
|
+
- Configurable pagination URL/link output (`baseUrl`, `pageName`, key mapping)
|
|
36
40
|
- Predictable and consistent response contracts
|
|
37
41
|
- Strong TypeScript typing
|
|
38
42
|
- Transport-layer friendly (Express, H3, and others)
|
|
@@ -93,10 +97,13 @@ class UserCollection<R extends User[]> extends ResourceCollection<R> {
|
|
|
93
97
|
return new UserCollection({
|
|
94
98
|
data: users,
|
|
95
99
|
pagination: {
|
|
100
|
+
currentPage: 1,
|
|
101
|
+
lastPage: 10,
|
|
96
102
|
from: 1,
|
|
97
103
|
to: 10,
|
|
98
104
|
perPage: 10,
|
|
99
105
|
total: 100,
|
|
106
|
+
path: '/users',
|
|
100
107
|
},
|
|
101
108
|
}).additional({
|
|
102
109
|
status: 'success',
|
|
@@ -109,13 +116,17 @@ Response:
|
|
|
109
116
|
```json
|
|
110
117
|
{
|
|
111
118
|
"data": [...],
|
|
119
|
+
"links": {
|
|
120
|
+
"last": "https://localhost/users?page=10"
|
|
121
|
+
},
|
|
112
122
|
"meta": {
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
"from": 1,
|
|
124
|
+
"to": 10,
|
|
125
|
+
"per_page": 10,
|
|
126
|
+
"total": 100,
|
|
127
|
+
"current_page": 1,
|
|
128
|
+
"last_page": 10,
|
|
129
|
+
"path": "/users"
|
|
119
130
|
},
|
|
120
131
|
"status": "success",
|
|
121
132
|
"message": "Users retrieved"
|
|
@@ -162,6 +173,23 @@ It works with:
|
|
|
162
173
|
|
|
163
174
|
Adapters can be added without changing application logic.
|
|
164
175
|
|
|
176
|
+
## Conditional Rendering Example
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
class UserResource extends Resource {
|
|
180
|
+
data() {
|
|
181
|
+
return {
|
|
182
|
+
id: this.id,
|
|
183
|
+
email: this.whenNotNull(this.email),
|
|
184
|
+
role: this.when(this.isAdmin, 'admin'),
|
|
185
|
+
...this.mergeWhen(this.isAdmin, { permissions: ['manage-users'] }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Falsy/null attributes are omitted from the final serialized payload.
|
|
192
|
+
|
|
165
193
|
---
|
|
166
194
|
|
|
167
195
|
## When to Use Resora
|
|
@@ -177,6 +205,15 @@ It is intentionally not opinionated about routing, validation, or persistence.
|
|
|
177
205
|
|
|
178
206
|
---
|
|
179
207
|
|
|
208
|
+
## Documentation
|
|
209
|
+
|
|
210
|
+
- Getting Started: https://arcstack-hq.github.io/resora/guide/getting-started
|
|
211
|
+
- Configuration: https://arcstack-hq.github.io/resora/guide/configuration
|
|
212
|
+
- Conditional Rendering: https://arcstack-hq.github.io/resora/guide/conditional-attributes
|
|
213
|
+
- Pagination & Cursor Recipes: https://arcstack-hq.github.io/resora/guide/pagination-cursor-recipes
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
180
217
|
## License
|
|
181
218
|
|
|
182
219
|
MIT
|
package/bin/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 l,Kernel as u}from"@h3ravel/musket";let d=o.resolve(process.cwd(),`node_modules/resora/stubs`);t(d)||(d=o.resolve(process.cwd(),`stubs`));const f=()=>({stubsDir:d,preferredCase:`camel`,paginatedExtras:[`meta`,`links`],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`},resourcesDir:`src/resources`,stubs:{config:`resora.config.stub`,resource:`resource.stub`,collection:`resource.collection.stub`}}),p=(e={})=>{let t=f();return Object.assign(t,e,{stubs:Object.assign(t.stubs,e.stubs||{})})};var m=class{command;config={};constructor(e={}){this.config=p(e)}async loadConfig(e={}){this.config=p(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}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=`/**
|
|
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 l,Kernel as u}from"@h3ravel/musket";let d=o.resolve(process.cwd(),`node_modules/resora/stubs`);t(d)||(d=o.resolve(process.cwd(),`stubs`));const f=()=>({stubsDir:d,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`}}),p=(e={})=>{let t=f();return Object.assign(t,e,{stubs:Object.assign(t.stubs,e.stubs||{})})};var m=class{command;config={};constructor(e={}){this.config=p(e)}async loadConfig(e={}){this.config=p(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}
|
|
@@ -26,4 +26,4 @@ import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,rmSync
|
|
|
26
26
|
| _ // _ \/ __|/ _ \| '__/ _, |
|
|
27
27
|
| | \ \ __/\__ \ (_) | | | (_| |
|
|
28
28
|
|_| \_\___||___/\___/|_| \__,_|
|
|
29
|
-
`;const v=new m;await u.init(await v.loadConfig(),{logo:_,name:`Resora CLI`,baseCommands:[g,h],exceptionHandler(e){throw e}});export{};
|
|
29
|
+
`;const v=new m;await u.init(await v.loadConfig(),{logo:_,name:`Resora CLI`,baseCommands:[g,h,...v.getConfig().extraCommands||[]],exceptionHandler(e){throw e}});export{};
|