resora 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Resora
2
2
 
3
+ [![NPM Downloads](https://img.shields.io/npm/dt/resora.svg)](https://www.npmjs.com/package/resora)
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)
8
+
3
9
  Resora is a structured API response layer for Node.js and TypeScript backends.
4
10
 
5
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.
package/bin/index.mjs ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env ts-node
2
+ import path, { dirname, join } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
4
+ import { createRequire } from "module";
5
+ import { fileURLToPath } from "url";
6
+ import { Command, Kernel } from "@h3ravel/musket";
7
+
8
+ //#region src/utility.ts
9
+ const __dirname = /* @__PURE__ */ path.dirname(fileURLToPath(import.meta.url));
10
+ let stubsDir = path.resolve(__dirname, "../node_modules/resora/stubs");
11
+ if (!existsSync(stubsDir)) stubsDir = path.resolve(__dirname, "../stubs");
12
+ /**
13
+ * Define the configuration for the package
14
+ *
15
+ * @param userConfig The user configuration to override the default configuration
16
+ * @returns The merged configuration object
17
+ */
18
+ const defineConfig = (userConfig = {}) => {
19
+ return Object.assign({
20
+ resourcesDir: "src/resources",
21
+ stubsDir,
22
+ stubs: {
23
+ resource: "resource.stub",
24
+ collection: "resource.collection.stub"
25
+ }
26
+ }, userConfig, { stubs: Object.assign({
27
+ resource: "resource.stub",
28
+ collection: "resource.collection.stub"
29
+ }, userConfig.stubs || {}) });
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/cli/actions.ts
34
+ var CliApp = class {
35
+ command;
36
+ config = {};
37
+ constructor(config = {}) {
38
+ this.config = defineConfig(config);
39
+ const require = createRequire(import.meta.url);
40
+ const possibleConfigPaths = [
41
+ join(process.cwd(), "resora.config.ts"),
42
+ join(process.cwd(), "resora.config.js"),
43
+ join(process.cwd(), "resora.config.cjs")
44
+ ];
45
+ for (const configPath of possibleConfigPaths) if (existsSync(configPath)) try {
46
+ const { default: userConfig } = require(configPath);
47
+ Object.assign(this.config, defineConfig(userConfig));
48
+ break;
49
+ } catch (e) {
50
+ console.error(`Error loading config file at ${configPath}:`, e);
51
+ }
52
+ }
53
+ /**
54
+ * Utility to ensure directory exists
55
+ *
56
+ * @param filePath
57
+ */
58
+ ensureDirectory(filePath) {
59
+ const dir = dirname(filePath);
60
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
61
+ }
62
+ /**
63
+ * Utility to generate file from stub
64
+ *
65
+ * @param stubPath
66
+ * @param outputPath
67
+ * @param replacements
68
+ */
69
+ generateFile(stubPath, outputPath, replacements, options) {
70
+ if (existsSync(outputPath) && !options?.force) {
71
+ this.command.error(`Error: ${outputPath} already exists.`);
72
+ process.exit(1);
73
+ } else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
74
+ let content = readFileSync(stubPath, "utf-8");
75
+ for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
76
+ this.ensureDirectory(outputPath);
77
+ writeFileSync(outputPath, content);
78
+ return outputPath;
79
+ }
80
+ /**
81
+ * Command to create a new resource or resource collection file
82
+ *
83
+ * @param name
84
+ * @param options
85
+ */
86
+ makeResource(name, options) {
87
+ let resourceName = name;
88
+ if (options?.collection && !name.endsWith("Collection") && !name.endsWith("Resource")) resourceName += "Collection";
89
+ else if (!options?.collection && !name.endsWith("Resource") && !name.endsWith("Collection")) resourceName += "Resource";
90
+ const fileName = `${resourceName}.ts`;
91
+ const outputPath = join(this.config.resourcesDir, fileName);
92
+ const stubPath = join(this.config.stubsDir, options?.collection || name.endsWith("Collection") ? this.config.stubs.collection : this.config.stubs.resource);
93
+ if (!existsSync(stubPath)) {
94
+ this.command.error(`Error: Stub file ${stubPath} not found.`);
95
+ process.exit(1);
96
+ }
97
+ const collectsName = resourceName.replace(/(Resource|Collection)$/, "") + "Resource";
98
+ const collects = `/**
99
+ * The resource that this collection collects.
100
+ */
101
+ collects = ${collectsName}
102
+ `;
103
+ const collectsImport = `import ${collectsName} from './${collectsName}'\n`;
104
+ const hasCollects = (!!options?.collection || name.endsWith("Collection")) && existsSync(join(this.config.resourcesDir, `${collectsName}.ts`));
105
+ const path = this.generateFile(stubPath, outputPath, {
106
+ ResourceName: resourceName,
107
+ CollectionResourceName: resourceName.replace(/(Resource|Collection)$/, "") + "Resource",
108
+ "collects = Resource": hasCollects ? collects : "",
109
+ "import = Resource": hasCollects ? collectsImport : ""
110
+ }, options);
111
+ return {
112
+ name: resourceName,
113
+ path
114
+ };
115
+ }
116
+ };
117
+
118
+ //#endregion
119
+ //#region src/cli/commands/MakeResource.ts
120
+ var MakeResource = class extends Command {
121
+ signature = `#create:
122
+ {resource : Generates a new resource file.
123
+ | {name : Name of the resource to create}
124
+ | {--c|collection : Make a resource collection}
125
+ | {--force : Create the resource or collection file even if it already exists.}
126
+ }
127
+ {collection : Create a new resource collection file.
128
+ | {name : Name of the resource collection to create}
129
+ | {--force : Create the resource or collection file even if it already exists.}
130
+ }
131
+ {all : Create both resource and collection files.
132
+ | {prefix : prefix of the resources to create, "Admin" will create AdminResource, AdminCollection}
133
+ | {--force : Create the resource or collection file even if it already exists.}
134
+ }
135
+ `;
136
+ description = "Create a new resource or resource collection file";
137
+ async handle() {
138
+ this.app.command = this;
139
+ let path = "";
140
+ const action = this.dictionary.name || this.dictionary.baseCommand;
141
+ if (["resource", "collection"].includes(action) && !this.argument("name")) return void this.error("Error: Name argument is required.");
142
+ if (action === "all" && !this.argument("prefix")) return void this.error("Error: Prefix argument is required.");
143
+ switch (action) {
144
+ case "resource":
145
+ ({path} = this.app.makeResource(this.argument("name"), this.options()));
146
+ break;
147
+ case "collection":
148
+ ({path} = this.app.makeResource(this.argument("name") + "Collection", this.options()));
149
+ break;
150
+ case "all": {
151
+ const o1 = this.app.makeResource(this.argument("prefix"), { force: this.option("force") });
152
+ const o2 = this.app.makeResource(this.argument("prefix") + "Collection", {
153
+ collection: true,
154
+ force: this.option("force")
155
+ });
156
+ path = `${o1.path}, ${o2.path}`;
157
+ break;
158
+ }
159
+ default: this.fail(`Unknown action: ${action}`);
160
+ }
161
+ this.success(`Created: ${path}`);
162
+ }
163
+ };
164
+
165
+ //#endregion
166
+ //#region src/cli/logo.ts
167
+ var logo_default = String.raw`
168
+ _____
169
+ | __ \
170
+ | |__) |___ ___ ___ _ __ __ _
171
+ | _ // _ \/ __|/ _ \| '__/ _, |
172
+ | | \ \ __/\__ \ (_) | | | (_| |
173
+ |_| \_\___||___/\___/|_| \__,_|
174
+ `;
175
+
176
+ //#endregion
177
+ //#region src/cli/index.ts
178
+ const app = new CliApp();
179
+ await Kernel.init(app, {
180
+ logo: logo_default,
181
+ name: "Resora CLI",
182
+ baseCommands: [MakeResource],
183
+ exceptionHandler(exception) {
184
+ throw exception;
185
+ }
186
+ });
187
+
188
+ //#endregion
189
+ export { };
package/dist/index.cjs CHANGED
@@ -1,4 +1,37 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let path = require("path");
30
+ path = __toESM(path);
31
+ let fs = require("fs");
32
+ let module$1 = require("module");
33
+ let url = require("url");
34
+ let _h3ravel_musket = require("@h3ravel/musket");
2
35
 
3
36
  //#region src/ApiResource.ts
4
37
  /**
@@ -11,6 +44,175 @@ function ApiResource(instance) {
11
44
  return instance;
12
45
  }
13
46
 
47
+ //#endregion
48
+ //#region src/utility.ts
49
+ const __dirname$1 = /* @__PURE__ */ path.default.dirname((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
50
+ let stubsDir = path.default.resolve(__dirname$1, "../node_modules/resora/stubs");
51
+ if (!(0, fs.existsSync)(stubsDir)) stubsDir = path.default.resolve(__dirname$1, "../stubs");
52
+ /**
53
+ * Define the configuration for the package
54
+ *
55
+ * @param userConfig The user configuration to override the default configuration
56
+ * @returns The merged configuration object
57
+ */
58
+ const defineConfig = (userConfig = {}) => {
59
+ return Object.assign({
60
+ resourcesDir: "src/resources",
61
+ stubsDir,
62
+ stubs: {
63
+ resource: "resource.stub",
64
+ collection: "resource.collection.stub"
65
+ }
66
+ }, userConfig, { stubs: Object.assign({
67
+ resource: "resource.stub",
68
+ collection: "resource.collection.stub"
69
+ }, userConfig.stubs || {}) });
70
+ };
71
+
72
+ //#endregion
73
+ //#region src/cli/actions.ts
74
+ var CliApp = class {
75
+ command;
76
+ config = {};
77
+ constructor(config = {}) {
78
+ this.config = defineConfig(config);
79
+ const require = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
80
+ const possibleConfigPaths = [
81
+ (0, path.join)(process.cwd(), "resora.config.ts"),
82
+ (0, path.join)(process.cwd(), "resora.config.js"),
83
+ (0, path.join)(process.cwd(), "resora.config.cjs")
84
+ ];
85
+ for (const configPath of possibleConfigPaths) if ((0, fs.existsSync)(configPath)) try {
86
+ const { default: userConfig } = require(configPath);
87
+ Object.assign(this.config, defineConfig(userConfig));
88
+ break;
89
+ } catch (e) {
90
+ console.error(`Error loading config file at ${configPath}:`, e);
91
+ }
92
+ }
93
+ /**
94
+ * Utility to ensure directory exists
95
+ *
96
+ * @param filePath
97
+ */
98
+ ensureDirectory(filePath) {
99
+ const dir = (0, path.dirname)(filePath);
100
+ if (!(0, fs.existsSync)(dir)) (0, fs.mkdirSync)(dir, { recursive: true });
101
+ }
102
+ /**
103
+ * Utility to generate file from stub
104
+ *
105
+ * @param stubPath
106
+ * @param outputPath
107
+ * @param replacements
108
+ */
109
+ generateFile(stubPath, outputPath, replacements, options) {
110
+ if ((0, fs.existsSync)(outputPath) && !options?.force) {
111
+ this.command.error(`Error: ${outputPath} already exists.`);
112
+ process.exit(1);
113
+ } else if ((0, fs.existsSync)(outputPath) && options?.force) (0, fs.rmSync)(outputPath);
114
+ let content = (0, fs.readFileSync)(stubPath, "utf-8");
115
+ for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
116
+ this.ensureDirectory(outputPath);
117
+ (0, fs.writeFileSync)(outputPath, content);
118
+ return outputPath;
119
+ }
120
+ /**
121
+ * Command to create a new resource or resource collection file
122
+ *
123
+ * @param name
124
+ * @param options
125
+ */
126
+ makeResource(name, options) {
127
+ let resourceName = name;
128
+ if (options?.collection && !name.endsWith("Collection") && !name.endsWith("Resource")) resourceName += "Collection";
129
+ else if (!options?.collection && !name.endsWith("Resource") && !name.endsWith("Collection")) resourceName += "Resource";
130
+ const fileName = `${resourceName}.ts`;
131
+ const outputPath = (0, path.join)(this.config.resourcesDir, fileName);
132
+ const stubPath = (0, path.join)(this.config.stubsDir, options?.collection || name.endsWith("Collection") ? this.config.stubs.collection : this.config.stubs.resource);
133
+ if (!(0, fs.existsSync)(stubPath)) {
134
+ this.command.error(`Error: Stub file ${stubPath} not found.`);
135
+ process.exit(1);
136
+ }
137
+ const collectsName = resourceName.replace(/(Resource|Collection)$/, "") + "Resource";
138
+ const collects = `/**
139
+ * The resource that this collection collects.
140
+ */
141
+ collects = ${collectsName}
142
+ `;
143
+ const collectsImport = `import ${collectsName} from './${collectsName}'\n`;
144
+ const hasCollects = (!!options?.collection || name.endsWith("Collection")) && (0, fs.existsSync)((0, path.join)(this.config.resourcesDir, `${collectsName}.ts`));
145
+ const path$2 = this.generateFile(stubPath, outputPath, {
146
+ ResourceName: resourceName,
147
+ CollectionResourceName: resourceName.replace(/(Resource|Collection)$/, "") + "Resource",
148
+ "collects = Resource": hasCollects ? collects : "",
149
+ "import = Resource": hasCollects ? collectsImport : ""
150
+ }, options);
151
+ return {
152
+ name: resourceName,
153
+ path: path$2
154
+ };
155
+ }
156
+ };
157
+
158
+ //#endregion
159
+ //#region src/cli/commands/MakeResource.ts
160
+ var MakeResource = class extends _h3ravel_musket.Command {
161
+ signature = `#create:
162
+ {resource : Generates a new resource file.
163
+ | {name : Name of the resource to create}
164
+ | {--c|collection : Make a resource collection}
165
+ | {--force : Create the resource or collection file even if it already exists.}
166
+ }
167
+ {collection : Create a new resource collection file.
168
+ | {name : Name of the resource collection to create}
169
+ | {--force : Create the resource or collection file even if it already exists.}
170
+ }
171
+ {all : Create both resource and collection files.
172
+ | {prefix : prefix of the resources to create, "Admin" will create AdminResource, AdminCollection}
173
+ | {--force : Create the resource or collection file even if it already exists.}
174
+ }
175
+ `;
176
+ description = "Create a new resource or resource collection file";
177
+ async handle() {
178
+ this.app.command = this;
179
+ let path = "";
180
+ const action = this.dictionary.name || this.dictionary.baseCommand;
181
+ if (["resource", "collection"].includes(action) && !this.argument("name")) return void this.error("Error: Name argument is required.");
182
+ if (action === "all" && !this.argument("prefix")) return void this.error("Error: Prefix argument is required.");
183
+ switch (action) {
184
+ case "resource":
185
+ ({path} = this.app.makeResource(this.argument("name"), this.options()));
186
+ break;
187
+ case "collection":
188
+ ({path} = this.app.makeResource(this.argument("name") + "Collection", this.options()));
189
+ break;
190
+ case "all": {
191
+ const o1 = this.app.makeResource(this.argument("prefix"), { force: this.option("force") });
192
+ const o2 = this.app.makeResource(this.argument("prefix") + "Collection", {
193
+ collection: true,
194
+ force: this.option("force")
195
+ });
196
+ path = `${o1.path}, ${o2.path}`;
197
+ break;
198
+ }
199
+ default: this.fail(`Unknown action: ${action}`);
200
+ }
201
+ this.success(`Created: ${path}`);
202
+ }
203
+ };
204
+
205
+ //#endregion
206
+ //#region src/cli/logo.ts
207
+ var logo_default = String.raw`
208
+ _____
209
+ | __ \
210
+ | |__) |___ ___ ___ _ __ __ _
211
+ | _ // _ \/ __|/ _ \| '__/ _, |
212
+ | | \ \ __/\__ \ (_) | | | (_| |
213
+ |_| \_\___||___/\___/|_| \__,_|
214
+ `;
215
+
14
216
  //#endregion
15
217
  //#region src/ServerResponse.ts
16
218
  var ServerResponse = class {
@@ -125,6 +327,113 @@ var ServerResponse = class {
125
327
  }
126
328
  };
127
329
 
330
+ //#endregion
331
+ //#region src/GenericResource.ts
332
+ /**
333
+ * GenericResource class to handle API resource transformation and response building
334
+ */
335
+ var GenericResource = class {
336
+ body = { data: {} };
337
+ resource;
338
+ collects;
339
+ called = {};
340
+ constructor(rsc, res) {
341
+ this.res = res;
342
+ this.resource = rsc;
343
+ /**
344
+ * Copy properties from rsc to this instance for easy
345
+ * access, but only if data is not an array
346
+ */
347
+ if (!Array.isArray(this.resource.data ?? this.resource)) {
348
+ for (const key of Object.keys(this.resource.data ?? this.resource)) if (!(key in this)) Object.defineProperty(this, key, {
349
+ enumerable: true,
350
+ configurable: true,
351
+ get: () => {
352
+ return this.resource.data?.[key] ?? this.resource[key];
353
+ },
354
+ set: (value) => {
355
+ if (this.resource.data && this.resource.data[key]) this.resource.data[key] = value;
356
+ else this.resource[key] = value;
357
+ }
358
+ });
359
+ }
360
+ }
361
+ /**
362
+ * Get the original resource data
363
+ */
364
+ data() {
365
+ return this.resource;
366
+ }
367
+ /**
368
+ * Convert resource to JSON response format
369
+ *
370
+ * @returns
371
+ */
372
+ json() {
373
+ if (!this.called.json) {
374
+ this.called.json = true;
375
+ const resource = this.data();
376
+ let data = Array.isArray(resource) ? [...resource] : { ...resource };
377
+ if (Array.isArray(data) && this.collects) {
378
+ data = data.map((item) => new this.collects(item).data());
379
+ this.resource = data;
380
+ }
381
+ if (typeof data.data !== "undefined") data = data.data;
382
+ if (this.resource.pagination && data.data && Array.isArray(data.data)) delete data.pagination;
383
+ this.body = { data };
384
+ if (Array.isArray(this.body.data) && this.resource.pagination) this.body.meta = { pagination: this.resource.pagination };
385
+ }
386
+ return this;
387
+ }
388
+ /**
389
+ * Convert resource to array format (for collections)
390
+ *
391
+ * @returns
392
+ */
393
+ toArray() {
394
+ this.called.toArray = true;
395
+ this.json();
396
+ let data = Array.isArray(this.resource) ? [...this.resource] : { ...this.resource };
397
+ if (typeof data.data !== "undefined") data = data.data;
398
+ return data;
399
+ }
400
+ /**
401
+ * Add additional properties to the response body
402
+ *
403
+ * @param extra Additional properties to merge into the response body
404
+ * @returns
405
+ */
406
+ additional(extra) {
407
+ this.called.additional = true;
408
+ this.json();
409
+ delete extra.data;
410
+ delete extra.pagination;
411
+ this.body = {
412
+ ...this.body,
413
+ ...extra
414
+ };
415
+ return this;
416
+ }
417
+ response(res) {
418
+ this.called.toResponse = true;
419
+ return new ServerResponse(res ?? this.res, this.body);
420
+ }
421
+ /**
422
+ * Promise-like then method to allow chaining with async/await or .then() syntax
423
+ *
424
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
425
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
426
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
427
+ */
428
+ then(onfulfilled, onrejected) {
429
+ this.called.then = true;
430
+ this.json();
431
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
432
+ if (this.res) this.res.send(this.body);
433
+ return resolved;
434
+ }
435
+ };
436
+
128
437
  //#endregion
129
438
  //#region src/ResourceCollection.ts
130
439
  /**
@@ -368,4 +677,10 @@ var Resource = class {
368
677
 
369
678
  //#endregion
370
679
  exports.ApiResource = ApiResource;
371
- exports.Resource = Resource;
680
+ exports.CliApp = CliApp;
681
+ exports.GenericResource = GenericResource;
682
+ exports.MakeResource = MakeResource;
683
+ exports.Resource = Resource;
684
+ exports.ResourceCollection = ResourceCollection;
685
+ exports.ServerResponse = ServerResponse;
686
+ exports.defineConfig = defineConfig;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Command } from "@h3ravel/musket";
1
2
  import { H3Event } from "h3";
2
3
  import { Response } from "express";
3
4
 
@@ -17,7 +18,7 @@ interface MetaData {
17
18
  interface ResourceData {
18
19
  [key: string]: any;
19
20
  }
20
- interface Resource$1 extends ResourceData {
21
+ interface ResourceDef extends ResourceData {
21
22
  cursor?: Cursor | undefined;
22
23
  pagination?: Pagination | undefined;
23
24
  }
@@ -29,11 +30,38 @@ interface Collectible {
29
30
  cursor?: Cursor | undefined;
30
31
  pagination?: Pagination | undefined;
31
32
  }
32
- interface ResponseData<R extends ResourceData = any> extends Resource$1 {
33
+ interface ResponseData<R extends ResourceData = any> extends ResourceDef {
33
34
  data: R;
34
35
  meta?: MetaData | undefined;
35
36
  }
37
+ /**
38
+ * @description A type that represents the metadata for a paginated collection of resources. It extends the MetaData type and includes all properties of a Collectible object except for the data property. This type is used to provide additional information about the paginated collection, such as pagination details, without including the actual resource data.
39
+ * @example
40
+ * const paginatedMeta: PaginatedMetaData = {
41
+ * pagination: {
42
+ * currentPage: 1,
43
+ * total: 100
44
+ * },
45
+ * timestamp: '2024-06-01T12:00:00Z'
46
+ * };
47
+ */
36
48
  type PaginatedMetaData<R extends Collectible = Collectible> = MetaData & Omit<R, 'data'>;
49
+ /**
50
+ * @description A type that represents the body of a response for a collection of resources. It includes a data property, which can be either an array of ResourceData objects or a Collectible object, and an optional meta property that can contain any additional metadata related to the response. The type is generic and can be used to define the structure of the response body for API endpoints that return collections of resources.
51
+ * @example
52
+ * const collectionResponse: CollectionBody = {
53
+ * data: [
54
+ * { id: 1, name: 'Resource 1' },
55
+ * { id: 2, name: 'Resource 2' }
56
+ * ],
57
+ * meta: {
58
+ * pagination: {
59
+ * currentPage: 1,
60
+ * total: 2
61
+ * }
62
+ * }
63
+ * };
64
+ */
37
65
  interface ResponseDataCollection<R extends Collectible | undefined = undefined> extends ResourceData {
38
66
  data: R extends Collectible ? R['data'] : R;
39
67
  meta?: R extends Collectible ? PaginatedMetaData<R> | undefined : undefined;
@@ -80,6 +108,23 @@ type CollectionBody<R extends ResourceData[] | Collectible = ResourceData[]> = R
80
108
  type ResourceBody<R extends ResourceData | NonCollectible = ResourceData> = ResponseData<R extends NonCollectible ? R : {
81
109
  data: R;
82
110
  }>;
111
+ /**
112
+ * @description A type that represents the body of a response for either a single resource or a collection of resources.
113
+ * It can be either a ResourceData object, an array of ResourceData objects, a NonCollectible object, or a Collectible object.
114
+ * The type also includes the meta property, which is optional and can contain any additional metadata related to the response.
115
+ * @example
116
+ * const genericResponse: GenericBody = {
117
+ * data: {
118
+ * id: 1,
119
+ * name: 'Resource Name',
120
+ * description: 'Resource Description'
121
+ * },
122
+ * meta: {
123
+ * timestamp: '2024-06-01T12:00:00Z'
124
+ * }
125
+ * };
126
+ */
127
+ type GenericBody<R extends NonCollectible | Collectible | ResourceData = ResourceData> = ResponseData<R>;
83
128
  /**
84
129
  * @description A type that represents the pagination information for a collection of resources. It includes properties such as currentPage, from, to, perPage, total, firstPage, lastPage, prevPage, and nextPage. All properties are optional and can be undefined if not applicable.
85
130
  * @example
@@ -118,6 +163,67 @@ interface Cursor {
118
163
  previous?: string | undefined;
119
164
  next?: string | undefined;
120
165
  }
166
+ interface Config {
167
+ /**
168
+ * @description The directory where resource files are stored. This is the location where the generated resource files will be saved. It should be a valid path on the file system.
169
+ */
170
+ resourcesDir: string;
171
+ /**
172
+ * @description The directory where stub files are stored. Stub files are templates used for generating resource files. This should also be a valid path on the file system where the stub templates are located.
173
+ */
174
+ stubsDir: string;
175
+ /**
176
+ * @description An object that defines the stub file names for different types of resources.
177
+ */
178
+ stubs: {
179
+ /**
180
+ * @description The stub file name for a resource. This stub will be used when generating a resource file.
181
+ */
182
+ resource: string;
183
+ /**
184
+ * @description The stub file name for a collection resource. This stub will be used when generating a collection resource file.
185
+ */
186
+ collection: string;
187
+ };
188
+ }
189
+ //#endregion
190
+ //#region src/cli/actions.d.ts
191
+ declare class CliApp {
192
+ command: Command;
193
+ private config;
194
+ constructor(config?: Partial<Config>);
195
+ /**
196
+ * Utility to ensure directory exists
197
+ *
198
+ * @param filePath
199
+ */
200
+ ensureDirectory(filePath: string): void;
201
+ /**
202
+ * Utility to generate file from stub
203
+ *
204
+ * @param stubPath
205
+ * @param outputPath
206
+ * @param replacements
207
+ */
208
+ generateFile(stubPath: string, outputPath: string, replacements: Record<string, string>, options?: any): string;
209
+ /**
210
+ * Command to create a new resource or resource collection file
211
+ *
212
+ * @param name
213
+ * @param options
214
+ */
215
+ makeResource(name: string, options: any): {
216
+ name: string;
217
+ path: string;
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/cli/commands/MakeResource.d.ts
222
+ declare class MakeResource extends Command<CliApp> {
223
+ protected signature: string;
224
+ protected description: string;
225
+ handle(): Promise<undefined>;
226
+ }
121
227
  //#endregion
122
228
  //#region src/ServerResponse.d.ts
123
229
  declare class ServerResponse<R extends NonCollectible | Collectible | ResourceData[] | ResourceData = ResourceData> {
@@ -325,4 +431,62 @@ declare class Resource<R extends ResourceData | NonCollectible = ResourceData> {
325
431
  finally(onfinally?: (() => void) | null): Promise<void>;
326
432
  }
327
433
  //#endregion
328
- export { ApiResource, Resource };
434
+ //#region src/GenericResource.d.ts
435
+ /**
436
+ * GenericResource class to handle API resource transformation and response building
437
+ */
438
+ declare class GenericResource<R extends NonCollectible | Collectible | ResourceData = ResourceData, T extends ResourceData = any> {
439
+ private res?;
440
+ [key: string]: any;
441
+ body: GenericBody<R>;
442
+ resource: R;
443
+ collects?: typeof Resource<T>;
444
+ private called;
445
+ constructor(rsc: R, res?: Response | undefined);
446
+ /**
447
+ * Get the original resource data
448
+ */
449
+ data(): R;
450
+ /**
451
+ * Convert resource to JSON response format
452
+ *
453
+ * @returns
454
+ */
455
+ json(): this;
456
+ /**
457
+ * Convert resource to array format (for collections)
458
+ *
459
+ * @returns
460
+ */
461
+ toArray(): any;
462
+ /**
463
+ * Add additional properties to the response body
464
+ *
465
+ * @param extra Additional properties to merge into the response body
466
+ * @returns
467
+ */
468
+ additional<X extends Record<string, any>>(extra: X): this;
469
+ response(): ServerResponse<GenericBody<R>>;
470
+ response(res: H3Event['res']): ServerResponse<GenericBody<R>>;
471
+ /**
472
+ * Promise-like then method to allow chaining with async/await or .then() syntax
473
+ *
474
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
475
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
476
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
477
+ */
478
+ then<TResult1 = GenericBody<R>, TResult2 = never>(onfulfilled?: ((value: GenericBody<R>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
479
+ }
480
+ //#endregion
481
+ //#region src/utility.d.ts
482
+ /**
483
+ * Define the configuration for the package
484
+ *
485
+ * @param userConfig The user configuration to override the default configuration
486
+ * @returns The merged configuration object
487
+ */
488
+ declare const defineConfig: (userConfig?: Partial<Omit<Config, "stubs">> & {
489
+ stubs?: Partial<Config["stubs"]>;
490
+ }) => Config;
491
+ //#endregion
492
+ export { ApiResource, CliApp, Collectible, CollectionBody, Config, Cursor, GenericBody, GenericResource, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResponseData, ResponseDataCollection, ServerResponse, defineConfig };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Command } from "@h3ravel/musket";
1
2
  import { H3Event } from "h3";
2
3
  import { Response } from "express";
3
4
 
@@ -17,7 +18,7 @@ interface MetaData {
17
18
  interface ResourceData {
18
19
  [key: string]: any;
19
20
  }
20
- interface Resource$1 extends ResourceData {
21
+ interface ResourceDef extends ResourceData {
21
22
  cursor?: Cursor | undefined;
22
23
  pagination?: Pagination | undefined;
23
24
  }
@@ -29,11 +30,38 @@ interface Collectible {
29
30
  cursor?: Cursor | undefined;
30
31
  pagination?: Pagination | undefined;
31
32
  }
32
- interface ResponseData<R extends ResourceData = any> extends Resource$1 {
33
+ interface ResponseData<R extends ResourceData = any> extends ResourceDef {
33
34
  data: R;
34
35
  meta?: MetaData | undefined;
35
36
  }
37
+ /**
38
+ * @description A type that represents the metadata for a paginated collection of resources. It extends the MetaData type and includes all properties of a Collectible object except for the data property. This type is used to provide additional information about the paginated collection, such as pagination details, without including the actual resource data.
39
+ * @example
40
+ * const paginatedMeta: PaginatedMetaData = {
41
+ * pagination: {
42
+ * currentPage: 1,
43
+ * total: 100
44
+ * },
45
+ * timestamp: '2024-06-01T12:00:00Z'
46
+ * };
47
+ */
36
48
  type PaginatedMetaData<R extends Collectible = Collectible> = MetaData & Omit<R, 'data'>;
49
+ /**
50
+ * @description A type that represents the body of a response for a collection of resources. It includes a data property, which can be either an array of ResourceData objects or a Collectible object, and an optional meta property that can contain any additional metadata related to the response. The type is generic and can be used to define the structure of the response body for API endpoints that return collections of resources.
51
+ * @example
52
+ * const collectionResponse: CollectionBody = {
53
+ * data: [
54
+ * { id: 1, name: 'Resource 1' },
55
+ * { id: 2, name: 'Resource 2' }
56
+ * ],
57
+ * meta: {
58
+ * pagination: {
59
+ * currentPage: 1,
60
+ * total: 2
61
+ * }
62
+ * }
63
+ * };
64
+ */
37
65
  interface ResponseDataCollection<R extends Collectible | undefined = undefined> extends ResourceData {
38
66
  data: R extends Collectible ? R['data'] : R;
39
67
  meta?: R extends Collectible ? PaginatedMetaData<R> | undefined : undefined;
@@ -80,6 +108,23 @@ type CollectionBody<R extends ResourceData[] | Collectible = ResourceData[]> = R
80
108
  type ResourceBody<R extends ResourceData | NonCollectible = ResourceData> = ResponseData<R extends NonCollectible ? R : {
81
109
  data: R;
82
110
  }>;
111
+ /**
112
+ * @description A type that represents the body of a response for either a single resource or a collection of resources.
113
+ * It can be either a ResourceData object, an array of ResourceData objects, a NonCollectible object, or a Collectible object.
114
+ * The type also includes the meta property, which is optional and can contain any additional metadata related to the response.
115
+ * @example
116
+ * const genericResponse: GenericBody = {
117
+ * data: {
118
+ * id: 1,
119
+ * name: 'Resource Name',
120
+ * description: 'Resource Description'
121
+ * },
122
+ * meta: {
123
+ * timestamp: '2024-06-01T12:00:00Z'
124
+ * }
125
+ * };
126
+ */
127
+ type GenericBody<R extends NonCollectible | Collectible | ResourceData = ResourceData> = ResponseData<R>;
83
128
  /**
84
129
  * @description A type that represents the pagination information for a collection of resources. It includes properties such as currentPage, from, to, perPage, total, firstPage, lastPage, prevPage, and nextPage. All properties are optional and can be undefined if not applicable.
85
130
  * @example
@@ -118,6 +163,67 @@ interface Cursor {
118
163
  previous?: string | undefined;
119
164
  next?: string | undefined;
120
165
  }
166
+ interface Config {
167
+ /**
168
+ * @description The directory where resource files are stored. This is the location where the generated resource files will be saved. It should be a valid path on the file system.
169
+ */
170
+ resourcesDir: string;
171
+ /**
172
+ * @description The directory where stub files are stored. Stub files are templates used for generating resource files. This should also be a valid path on the file system where the stub templates are located.
173
+ */
174
+ stubsDir: string;
175
+ /**
176
+ * @description An object that defines the stub file names for different types of resources.
177
+ */
178
+ stubs: {
179
+ /**
180
+ * @description The stub file name for a resource. This stub will be used when generating a resource file.
181
+ */
182
+ resource: string;
183
+ /**
184
+ * @description The stub file name for a collection resource. This stub will be used when generating a collection resource file.
185
+ */
186
+ collection: string;
187
+ };
188
+ }
189
+ //#endregion
190
+ //#region src/cli/actions.d.ts
191
+ declare class CliApp {
192
+ command: Command;
193
+ private config;
194
+ constructor(config?: Partial<Config>);
195
+ /**
196
+ * Utility to ensure directory exists
197
+ *
198
+ * @param filePath
199
+ */
200
+ ensureDirectory(filePath: string): void;
201
+ /**
202
+ * Utility to generate file from stub
203
+ *
204
+ * @param stubPath
205
+ * @param outputPath
206
+ * @param replacements
207
+ */
208
+ generateFile(stubPath: string, outputPath: string, replacements: Record<string, string>, options?: any): string;
209
+ /**
210
+ * Command to create a new resource or resource collection file
211
+ *
212
+ * @param name
213
+ * @param options
214
+ */
215
+ makeResource(name: string, options: any): {
216
+ name: string;
217
+ path: string;
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/cli/commands/MakeResource.d.ts
222
+ declare class MakeResource extends Command<CliApp> {
223
+ protected signature: string;
224
+ protected description: string;
225
+ handle(): Promise<undefined>;
226
+ }
121
227
  //#endregion
122
228
  //#region src/ServerResponse.d.ts
123
229
  declare class ServerResponse<R extends NonCollectible | Collectible | ResourceData[] | ResourceData = ResourceData> {
@@ -325,4 +431,62 @@ declare class Resource<R extends ResourceData | NonCollectible = ResourceData> {
325
431
  finally(onfinally?: (() => void) | null): Promise<void>;
326
432
  }
327
433
  //#endregion
328
- export { ApiResource, Resource };
434
+ //#region src/GenericResource.d.ts
435
+ /**
436
+ * GenericResource class to handle API resource transformation and response building
437
+ */
438
+ declare class GenericResource<R extends NonCollectible | Collectible | ResourceData = ResourceData, T extends ResourceData = any> {
439
+ private res?;
440
+ [key: string]: any;
441
+ body: GenericBody<R>;
442
+ resource: R;
443
+ collects?: typeof Resource<T>;
444
+ private called;
445
+ constructor(rsc: R, res?: Response | undefined);
446
+ /**
447
+ * Get the original resource data
448
+ */
449
+ data(): R;
450
+ /**
451
+ * Convert resource to JSON response format
452
+ *
453
+ * @returns
454
+ */
455
+ json(): this;
456
+ /**
457
+ * Convert resource to array format (for collections)
458
+ *
459
+ * @returns
460
+ */
461
+ toArray(): any;
462
+ /**
463
+ * Add additional properties to the response body
464
+ *
465
+ * @param extra Additional properties to merge into the response body
466
+ * @returns
467
+ */
468
+ additional<X extends Record<string, any>>(extra: X): this;
469
+ response(): ServerResponse<GenericBody<R>>;
470
+ response(res: H3Event['res']): ServerResponse<GenericBody<R>>;
471
+ /**
472
+ * Promise-like then method to allow chaining with async/await or .then() syntax
473
+ *
474
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
475
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
476
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
477
+ */
478
+ then<TResult1 = GenericBody<R>, TResult2 = never>(onfulfilled?: ((value: GenericBody<R>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
479
+ }
480
+ //#endregion
481
+ //#region src/utility.d.ts
482
+ /**
483
+ * Define the configuration for the package
484
+ *
485
+ * @param userConfig The user configuration to override the default configuration
486
+ * @returns The merged configuration object
487
+ */
488
+ declare const defineConfig: (userConfig?: Partial<Omit<Config, "stubs">> & {
489
+ stubs?: Partial<Config["stubs"]>;
490
+ }) => Config;
491
+ //#endregion
492
+ export { ApiResource, CliApp, Collectible, CollectionBody, Config, Cursor, GenericBody, GenericResource, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResponseData, ResponseDataCollection, ServerResponse, defineConfig };
package/dist/index.mjs CHANGED
@@ -1,3 +1,9 @@
1
+ import path, { dirname, join } from "path";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { createRequire } from "module";
4
+ import { fileURLToPath } from "url";
5
+ import { Command } from "@h3ravel/musket";
6
+
1
7
  //#region src/ApiResource.ts
2
8
  /**
3
9
  * ApiResource function to return the Resource instance
@@ -9,6 +15,175 @@ function ApiResource(instance) {
9
15
  return instance;
10
16
  }
11
17
 
18
+ //#endregion
19
+ //#region src/utility.ts
20
+ const __dirname = /* @__PURE__ */ path.dirname(fileURLToPath(import.meta.url));
21
+ let stubsDir = path.resolve(__dirname, "../node_modules/resora/stubs");
22
+ if (!existsSync(stubsDir)) stubsDir = path.resolve(__dirname, "../stubs");
23
+ /**
24
+ * Define the configuration for the package
25
+ *
26
+ * @param userConfig The user configuration to override the default configuration
27
+ * @returns The merged configuration object
28
+ */
29
+ const defineConfig = (userConfig = {}) => {
30
+ return Object.assign({
31
+ resourcesDir: "src/resources",
32
+ stubsDir,
33
+ stubs: {
34
+ resource: "resource.stub",
35
+ collection: "resource.collection.stub"
36
+ }
37
+ }, userConfig, { stubs: Object.assign({
38
+ resource: "resource.stub",
39
+ collection: "resource.collection.stub"
40
+ }, userConfig.stubs || {}) });
41
+ };
42
+
43
+ //#endregion
44
+ //#region src/cli/actions.ts
45
+ var CliApp = class {
46
+ command;
47
+ config = {};
48
+ constructor(config = {}) {
49
+ this.config = defineConfig(config);
50
+ const require = createRequire(import.meta.url);
51
+ const possibleConfigPaths = [
52
+ join(process.cwd(), "resora.config.ts"),
53
+ join(process.cwd(), "resora.config.js"),
54
+ join(process.cwd(), "resora.config.cjs")
55
+ ];
56
+ for (const configPath of possibleConfigPaths) if (existsSync(configPath)) try {
57
+ const { default: userConfig } = require(configPath);
58
+ Object.assign(this.config, defineConfig(userConfig));
59
+ break;
60
+ } catch (e) {
61
+ console.error(`Error loading config file at ${configPath}:`, e);
62
+ }
63
+ }
64
+ /**
65
+ * Utility to ensure directory exists
66
+ *
67
+ * @param filePath
68
+ */
69
+ ensureDirectory(filePath) {
70
+ const dir = dirname(filePath);
71
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
72
+ }
73
+ /**
74
+ * Utility to generate file from stub
75
+ *
76
+ * @param stubPath
77
+ * @param outputPath
78
+ * @param replacements
79
+ */
80
+ generateFile(stubPath, outputPath, replacements, options) {
81
+ if (existsSync(outputPath) && !options?.force) {
82
+ this.command.error(`Error: ${outputPath} already exists.`);
83
+ process.exit(1);
84
+ } else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
85
+ let content = readFileSync(stubPath, "utf-8");
86
+ for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
87
+ this.ensureDirectory(outputPath);
88
+ writeFileSync(outputPath, content);
89
+ return outputPath;
90
+ }
91
+ /**
92
+ * Command to create a new resource or resource collection file
93
+ *
94
+ * @param name
95
+ * @param options
96
+ */
97
+ makeResource(name, options) {
98
+ let resourceName = name;
99
+ if (options?.collection && !name.endsWith("Collection") && !name.endsWith("Resource")) resourceName += "Collection";
100
+ else if (!options?.collection && !name.endsWith("Resource") && !name.endsWith("Collection")) resourceName += "Resource";
101
+ const fileName = `${resourceName}.ts`;
102
+ const outputPath = join(this.config.resourcesDir, fileName);
103
+ const stubPath = join(this.config.stubsDir, options?.collection || name.endsWith("Collection") ? this.config.stubs.collection : this.config.stubs.resource);
104
+ if (!existsSync(stubPath)) {
105
+ this.command.error(`Error: Stub file ${stubPath} not found.`);
106
+ process.exit(1);
107
+ }
108
+ const collectsName = resourceName.replace(/(Resource|Collection)$/, "") + "Resource";
109
+ const collects = `/**
110
+ * The resource that this collection collects.
111
+ */
112
+ collects = ${collectsName}
113
+ `;
114
+ const collectsImport = `import ${collectsName} from './${collectsName}'\n`;
115
+ const hasCollects = (!!options?.collection || name.endsWith("Collection")) && existsSync(join(this.config.resourcesDir, `${collectsName}.ts`));
116
+ const path = this.generateFile(stubPath, outputPath, {
117
+ ResourceName: resourceName,
118
+ CollectionResourceName: resourceName.replace(/(Resource|Collection)$/, "") + "Resource",
119
+ "collects = Resource": hasCollects ? collects : "",
120
+ "import = Resource": hasCollects ? collectsImport : ""
121
+ }, options);
122
+ return {
123
+ name: resourceName,
124
+ path
125
+ };
126
+ }
127
+ };
128
+
129
+ //#endregion
130
+ //#region src/cli/commands/MakeResource.ts
131
+ var MakeResource = class extends Command {
132
+ signature = `#create:
133
+ {resource : Generates a new resource file.
134
+ | {name : Name of the resource to create}
135
+ | {--c|collection : Make a resource collection}
136
+ | {--force : Create the resource or collection file even if it already exists.}
137
+ }
138
+ {collection : Create a new resource collection file.
139
+ | {name : Name of the resource collection to create}
140
+ | {--force : Create the resource or collection file even if it already exists.}
141
+ }
142
+ {all : Create both resource and collection files.
143
+ | {prefix : prefix of the resources to create, "Admin" will create AdminResource, AdminCollection}
144
+ | {--force : Create the resource or collection file even if it already exists.}
145
+ }
146
+ `;
147
+ description = "Create a new resource or resource collection file";
148
+ async handle() {
149
+ this.app.command = this;
150
+ let path = "";
151
+ const action = this.dictionary.name || this.dictionary.baseCommand;
152
+ if (["resource", "collection"].includes(action) && !this.argument("name")) return void this.error("Error: Name argument is required.");
153
+ if (action === "all" && !this.argument("prefix")) return void this.error("Error: Prefix argument is required.");
154
+ switch (action) {
155
+ case "resource":
156
+ ({path} = this.app.makeResource(this.argument("name"), this.options()));
157
+ break;
158
+ case "collection":
159
+ ({path} = this.app.makeResource(this.argument("name") + "Collection", this.options()));
160
+ break;
161
+ case "all": {
162
+ const o1 = this.app.makeResource(this.argument("prefix"), { force: this.option("force") });
163
+ const o2 = this.app.makeResource(this.argument("prefix") + "Collection", {
164
+ collection: true,
165
+ force: this.option("force")
166
+ });
167
+ path = `${o1.path}, ${o2.path}`;
168
+ break;
169
+ }
170
+ default: this.fail(`Unknown action: ${action}`);
171
+ }
172
+ this.success(`Created: ${path}`);
173
+ }
174
+ };
175
+
176
+ //#endregion
177
+ //#region src/cli/logo.ts
178
+ var logo_default = String.raw`
179
+ _____
180
+ | __ \
181
+ | |__) |___ ___ ___ _ __ __ _
182
+ | _ // _ \/ __|/ _ \| '__/ _, |
183
+ | | \ \ __/\__ \ (_) | | | (_| |
184
+ |_| \_\___||___/\___/|_| \__,_|
185
+ `;
186
+
12
187
  //#endregion
13
188
  //#region src/ServerResponse.ts
14
189
  var ServerResponse = class {
@@ -123,6 +298,113 @@ var ServerResponse = class {
123
298
  }
124
299
  };
125
300
 
301
+ //#endregion
302
+ //#region src/GenericResource.ts
303
+ /**
304
+ * GenericResource class to handle API resource transformation and response building
305
+ */
306
+ var GenericResource = class {
307
+ body = { data: {} };
308
+ resource;
309
+ collects;
310
+ called = {};
311
+ constructor(rsc, res) {
312
+ this.res = res;
313
+ this.resource = rsc;
314
+ /**
315
+ * Copy properties from rsc to this instance for easy
316
+ * access, but only if data is not an array
317
+ */
318
+ if (!Array.isArray(this.resource.data ?? this.resource)) {
319
+ for (const key of Object.keys(this.resource.data ?? this.resource)) if (!(key in this)) Object.defineProperty(this, key, {
320
+ enumerable: true,
321
+ configurable: true,
322
+ get: () => {
323
+ return this.resource.data?.[key] ?? this.resource[key];
324
+ },
325
+ set: (value) => {
326
+ if (this.resource.data && this.resource.data[key]) this.resource.data[key] = value;
327
+ else this.resource[key] = value;
328
+ }
329
+ });
330
+ }
331
+ }
332
+ /**
333
+ * Get the original resource data
334
+ */
335
+ data() {
336
+ return this.resource;
337
+ }
338
+ /**
339
+ * Convert resource to JSON response format
340
+ *
341
+ * @returns
342
+ */
343
+ json() {
344
+ if (!this.called.json) {
345
+ this.called.json = true;
346
+ const resource = this.data();
347
+ let data = Array.isArray(resource) ? [...resource] : { ...resource };
348
+ if (Array.isArray(data) && this.collects) {
349
+ data = data.map((item) => new this.collects(item).data());
350
+ this.resource = data;
351
+ }
352
+ if (typeof data.data !== "undefined") data = data.data;
353
+ if (this.resource.pagination && data.data && Array.isArray(data.data)) delete data.pagination;
354
+ this.body = { data };
355
+ if (Array.isArray(this.body.data) && this.resource.pagination) this.body.meta = { pagination: this.resource.pagination };
356
+ }
357
+ return this;
358
+ }
359
+ /**
360
+ * Convert resource to array format (for collections)
361
+ *
362
+ * @returns
363
+ */
364
+ toArray() {
365
+ this.called.toArray = true;
366
+ this.json();
367
+ let data = Array.isArray(this.resource) ? [...this.resource] : { ...this.resource };
368
+ if (typeof data.data !== "undefined") data = data.data;
369
+ return data;
370
+ }
371
+ /**
372
+ * Add additional properties to the response body
373
+ *
374
+ * @param extra Additional properties to merge into the response body
375
+ * @returns
376
+ */
377
+ additional(extra) {
378
+ this.called.additional = true;
379
+ this.json();
380
+ delete extra.data;
381
+ delete extra.pagination;
382
+ this.body = {
383
+ ...this.body,
384
+ ...extra
385
+ };
386
+ return this;
387
+ }
388
+ response(res) {
389
+ this.called.toResponse = true;
390
+ return new ServerResponse(res ?? this.res, this.body);
391
+ }
392
+ /**
393
+ * Promise-like then method to allow chaining with async/await or .then() syntax
394
+ *
395
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
396
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
397
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
398
+ */
399
+ then(onfulfilled, onrejected) {
400
+ this.called.then = true;
401
+ this.json();
402
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
403
+ if (this.res) this.res.send(this.body);
404
+ return resolved;
405
+ }
406
+ };
407
+
126
408
  //#endregion
127
409
  //#region src/ResourceCollection.ts
128
410
  /**
@@ -365,4 +647,4 @@ var Resource = class {
365
647
  };
366
648
 
367
649
  //#endregion
368
- export { ApiResource, Resource };
650
+ export { ApiResource, CliApp, GenericResource, MakeResource, Resource, ResourceCollection, ServerResponse, defineConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resora",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A structured API response layer for Node.js and TypeScript with automatic JSON responses, collection support, and pagination handling.",
5
5
  "keywords": [
6
6
  "api",
@@ -37,9 +37,14 @@
37
37
  },
38
38
  "files": [
39
39
  "dist",
40
+ "stubs",
41
+ "bin",
40
42
  "README.md",
41
43
  "LICENSE"
42
44
  ],
45
+ "bin": {
46
+ "resora": "bin/index.mjs"
47
+ },
43
48
  "devDependencies": {
44
49
  "@eslint/js": "^10.0.1",
45
50
  "@eslint/markdown": "^7.5.1",
@@ -64,7 +69,11 @@
64
69
  "engines": {
65
70
  "node": ">=20.0.0"
66
71
  },
72
+ "dependencies": {
73
+ "@h3ravel/musket": "^0.10.1"
74
+ },
67
75
  "scripts": {
76
+ "cmd": "tsx src/cli/index.ts",
68
77
  "lint": "eslint",
69
78
  "test": "pnpm vitest",
70
79
  "test:coverage": "pnpm vitest --coverage --watch=false",
@@ -0,0 +1,16 @@
1
+ import { ResourceCollection } from 'resora'
2
+ {{import = Resource}}
3
+ /**
4
+ * {{ResourceName}}
5
+ */
6
+ export default class {{ResourceName}} extends ResourceCollection {
7
+ {{collects = Resource}}
8
+ /**
9
+ * Transform the resource into a plain JavaScript object.
10
+ *
11
+ * @memberof {{ResourceName}}
12
+ */
13
+ data () {
14
+ return this.toArray()
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { Resource } from 'resora'
2
+
3
+ /**
4
+ * {{ResourceName}}
5
+ */
6
+ export default class {{ResourceName}} extends Resource {
7
+ /**
8
+ * Transform the resource into a plain JavaScript object.
9
+ *
10
+ * @memberof {{ResourceName}}
11
+ */
12
+ data () {
13
+ return this.toArray()
14
+ }
15
+ }