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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![NPM Downloads](https://img.shields.io/npm/dt/resora.svg)](https://www.npmjs.com/package/resora)
4
4
  [![npm version](https://img.shields.io/npm/v/resora.svg)](https://www.npmjs.com/package/resora)
5
- [![License](https://img.shields.io/npm/l/resora.svg)](https://github.com/toneflix/resora/blob/main/LICENSE)
6
- [![CI](https://github.com/toneflix/resora/actions/workflows/ci.yml/badge.svg)](https://github.com/toneflix/resora/actions/workflows/ci.yml)
7
- [![Deploy Docs](https://github.com/toneflix/resora/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/toneflix/resora/actions/workflows/deploy-docs.yml)
5
+ [![License](https://img.shields.io/npm/l/resora.svg)](https://github.com/arcstack-hq/resora/blob/main/LICENSE)
6
+ [![CI](https://github.com/arcstack-hq/resora/actions/workflows/ci.yml/badge.svg)](https://github.com/arcstack-hq/resora/actions/workflows/ci.yml)
7
+ [![Deploy Docs](https://github.com/arcstack-hq/resora/actions/workflows/deploy-docs.yml/badge.svg)](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 pagination metadata while remaining framework-agnostic and strongly typed.
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
- "pagination": {
114
- "from": 1,
115
- "to": 10,
116
- "perPage": 10,
117
- "total": 100
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{};