@nano-app/frontend 0.1.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export * from "./api/ApiClient";
2
- export * from "./api/QueryBuilder";
3
- export { default as StoreManager } from "./store/StoreManager";
1
+ export * from "./store/ApiClient";
4
2
  export * from "./store/StoreClient";
5
- export * from "./ui/createComponent";
3
+ export * from "./store/StoreManager";
6
4
  export * from "./ui/Controller";
5
+ export * from "./ui/createComponent";
6
+ export * from "./utils/QueryBuilder";
7
7
  export * from "./utils/Toast";
8
8
  export * from "./utils/TokenHelper";
9
9
  export type { ApiResponse } from "@nano-app/backend";
package/dist/index.js CHANGED
@@ -13,17 +13,12 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
13
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
- var __importDefault = (this && this.__importDefault) || function (mod) {
17
- return (mod && mod.__esModule) ? mod : { "default": mod };
18
- };
19
16
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.StoreManager = void 0;
21
- __exportStar(require("./api/ApiClient"), exports);
22
- __exportStar(require("./api/QueryBuilder"), exports);
23
- var StoreManager_1 = require("./store/StoreManager");
24
- Object.defineProperty(exports, "StoreManager", { enumerable: true, get: function () { return __importDefault(StoreManager_1).default; } });
17
+ __exportStar(require("./store/ApiClient"), exports);
25
18
  __exportStar(require("./store/StoreClient"), exports);
26
- __exportStar(require("./ui/createComponent"), exports);
19
+ __exportStar(require("./store/StoreManager"), exports);
27
20
  __exportStar(require("./ui/Controller"), exports);
21
+ __exportStar(require("./ui/createComponent"), exports);
22
+ __exportStar(require("./utils/QueryBuilder"), exports);
28
23
  __exportStar(require("./utils/Toast"), exports);
29
24
  __exportStar(require("./utils/TokenHelper"), exports);
@@ -0,0 +1,12 @@
1
+ import type { ApiResponse } from "@nano-app/backend";
2
+ import { StoreClient } from "./StoreClient";
3
+ export type ApiRequest = (query: string, body?: unknown) => Promise<ApiResponse>;
4
+ export declare class ApiClient extends StoreClient {
5
+ private baseUrl;
6
+ constructor(baseUrl?: string);
7
+ create: ApiRequest;
8
+ read: ApiRequest;
9
+ update: ApiRequest;
10
+ delete: ApiRequest;
11
+ private request;
12
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiClient = void 0;
4
+ const StoreClient_1 = require("./StoreClient");
5
+ const QueryBuilder_1 = require("../utils/QueryBuilder");
6
+ const TokenHelper_1 = require("../utils/TokenHelper");
7
+ const Toast_1 = require("../utils/Toast");
8
+ class ApiClient extends StoreClient_1.StoreClient {
9
+ baseUrl;
10
+ constructor(baseUrl = "/api") {
11
+ super();
12
+ this.baseUrl = baseUrl;
13
+ }
14
+ create = (entity, body) => this.request("POST", entity, body);
15
+ read = async (query, params = {}) => {
16
+ const path = query.split("?")[0];
17
+ const response = await this.request("GET", QueryBuilder_1.QueryBuilder.build(query, params));
18
+ this.set(`/${path}`, response.result);
19
+ return response;
20
+ };
21
+ update = (entity, body) => this.request("PUT", entity, body);
22
+ delete = (entity, id) => this.request("DELETE", entity, id);
23
+ async request(method, path, body) {
24
+ let oResponse = { result: null, success: false, message: "", warning: false };
25
+ this.set("/busyCount", (this.get("/busyCount") || 0) + 1);
26
+ this.set("/busy", true);
27
+ try {
28
+ oResponse = await fetch(`${this.baseUrl}${path}`, {
29
+ method,
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${TokenHelper_1.TokenHelper.get()}`,
33
+ },
34
+ body: body ? JSON.stringify(body) : undefined,
35
+ })
36
+ .then((res) => res.json())
37
+ .then((res) => Toast_1.Toast.apiResponse(res));
38
+ }
39
+ catch (error) {
40
+ console.error(error);
41
+ }
42
+ finally {
43
+ const remaining = this.get("/busyCount") - 1;
44
+ this.set("/busyCount", remaining);
45
+ if (remaining < 1)
46
+ this.set("/busy", false);
47
+ }
48
+ return oResponse;
49
+ }
50
+ }
51
+ exports.ApiClient = ApiClient;
@@ -1,25 +1,11 @@
1
1
  export type StoreData = Record<string, any>;
2
- /**
3
- * Clase base para manejar el estado de Zustand
4
- * Zustand solo almacena valores, los métodos se ejecutan desde la clase
5
- */
6
2
  export declare class StoreClient {
7
3
  private getStore;
8
4
  private setFn;
9
5
  private getFn;
10
6
  constructor(initialData?: StoreData);
11
- /**
12
- * Ejecuta el hook para suscribirse al store (reactividad)
13
- */
14
7
  use: () => StoreData;
15
- /**
16
- * Setter y Getter del store
17
- */
18
8
  set(path: string, value: any): void;
19
9
  get(path: string): any;
20
- /**
21
- * Maneja el estado (Set o Get) navegando por el path
22
- * @private
23
- */
24
10
  private manageState;
25
11
  }
@@ -2,14 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StoreClient = void 0;
4
4
  const zustand_1 = require("zustand");
5
- /**
6
- * Clase base para manejar el estado de Zustand
7
- * Zustand solo almacena valores, los métodos se ejecutan desde la clase
8
- */
9
5
  class StoreClient {
10
6
  getStore;
11
- setFn = () => { };
12
- getFn = () => ({});
7
+ setFn;
8
+ getFn;
13
9
  constructor(initialData = {}) {
14
10
  this.getStore = (0, zustand_1.create)()((set, get) => {
15
11
  this.setFn = set;
@@ -17,48 +13,27 @@ class StoreClient {
17
13
  return initialData;
18
14
  });
19
15
  }
20
- /**
21
- * Ejecuta el hook para suscribirse al store (reactividad)
22
- */
23
16
  use = () => this.getStore();
24
- /**
25
- * Setter y Getter del store
26
- */
27
17
  set(path, value) {
28
- if (!this.setFn)
29
- return;
30
18
  const { oNewState } = this.manageState(path, true, value);
31
19
  this.setFn((state) => ({ ...state, ...oNewState }));
32
20
  }
33
21
  get(path) {
34
- if (!this.getFn)
35
- return undefined;
36
22
  const { value } = this.manageState(path);
37
23
  return value;
38
24
  }
39
- /**
40
- * Maneja el estado (Set o Get) navegando por el path
41
- * @private
42
- */
43
25
  manageState(sPath, isSet = false, value) {
44
- // Obtiene el path y el último valor
45
26
  const aPath = sPath.split("/").filter(s => s !== "");
46
27
  const lastProp = aPath.pop() || "";
47
- // Valida si es el path inicial (Utiliza todo el objeto)
48
28
  const oNewState = aPath.length === 0
49
29
  ? this.getFn()
50
- : // Llega al final del path generando objetos si es necesario
51
- aPath.reduce((oObject, sKey) => {
52
- if (!oObject[sKey]) {
53
- oObject[sKey] = {};
54
- }
55
- return oObject[sKey];
56
- }, this.getFn());
57
- // Setea el nuevo valor si debe
58
- if (isSet) {
30
+ : aPath.reduce((oObject, sKey) => {
31
+ if (!oObject[sKey])
32
+ oObject[sKey] = {};
33
+ return oObject[sKey];
34
+ }, this.getFn());
35
+ if (isSet)
59
36
  oNewState[lastProp] = value;
60
- }
61
- // Retorna el nuevo estado y el valor de la property enviada
62
37
  return { oNewState, value: lastProp ? oNewState[lastProp] : oNewState };
63
38
  }
64
39
  }
@@ -1,10 +1,15 @@
1
- import { ApiClient } from "../api/ApiClient";
1
+ import { ApiClient } from "./ApiClient";
2
2
  import { StoreClient } from "./StoreClient";
3
- export type StoresMap = Record<string, ApiClient | StoreClient>;
3
+ export type NanoAppConfig = Record<string, StoreEntry>;
4
+ export type StoreEntry = {
5
+ type: "api" | "local";
6
+ baseUrl?: string;
7
+ path?: string;
8
+ data?: any;
9
+ };
4
10
  export declare class StoreManager {
5
11
  private stores;
6
12
  constructor();
7
- addStore(name: string, store: ApiClient | StoreClient): void;
8
13
  getStore(name: string): ApiClient | StoreClient;
9
14
  }
10
15
  declare const _default: StoreManager;
@@ -5,54 +5,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.StoreManager = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
- const ApiClient_1 = require("../api/ApiClient");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const ApiClient_1 = require("./ApiClient");
9
10
  const StoreClient_1 = require("./StoreClient");
10
- let userConfig = {};
11
- try {
12
- // Carga nano-app.config.cjs desde la raíz del proyecto del usuario.
13
- // .cjs es CommonJS puro — Node.js lo ejecuta sincrónicamente sin compilador.
14
- // eslint-disable-next-line @typescript-eslint/no-require-imports
15
- const mod = require(path_1.default.join(process.cwd(), "nano-app.config.cjs"));
16
- userConfig = mod?.default ?? mod;
17
- }
18
- catch {
19
- // sin config → el manager empieza vacío
20
- }
21
- function readJsonPath(filePath) {
22
- try {
23
- // fs solo existe en Node.js — en el browser este bloque nunca se ejecuta
24
- // porque el StoreManager ya fue inicializado en el servidor
25
- // eslint-disable-next-line @typescript-eslint/no-require-imports
26
- const fs = require("fs");
27
- // eslint-disable-next-line @typescript-eslint/no-require-imports
28
- const path = require("path");
29
- const abs = path.resolve(process.cwd(), filePath);
30
- return JSON.parse(fs.readFileSync(abs, "utf-8"));
31
- }
32
- catch {
33
- return {};
34
- }
35
- }
36
11
  class StoreManager {
37
12
  stores = {};
38
13
  constructor() {
39
- if (!userConfig?.stores)
40
- return;
41
- Object.entries(userConfig.stores).forEach(([name, entry]) => {
14
+ // Carga la configuración
15
+ let config = { app: { type: "local", data: {} } };
16
+ try {
17
+ const mod = require(path_1.default.join(process.cwd(), "nano-app.config.cjs"));
18
+ config = mod?.default ?? mod;
19
+ }
20
+ catch { }
21
+ // Crea los stores
22
+ Object.entries(config)
23
+ .forEach(([name, entry]) => {
42
24
  if (entry.type === "api") {
43
25
  this.stores[name] = new ApiClient_1.ApiClient(entry.baseUrl ?? "/api");
44
26
  }
45
27
  else {
46
- const data = entry.path
47
- ? readJsonPath(entry.path)
48
- : (entry.data ?? {});
28
+ let data = {};
29
+ try {
30
+ data = entry.path
31
+ ? JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(process.cwd(), entry.path), "utf-8"))
32
+ : (entry.data ?? {});
33
+ }
34
+ catch { }
49
35
  this.stores[name] = new StoreClient_1.StoreClient(data);
50
36
  }
51
37
  });
52
38
  }
53
- addStore(name, store) {
54
- this.stores[name] = store;
55
- }
56
39
  getStore(name) {
57
40
  return this.stores[name];
58
41
  }
@@ -1,14 +1,17 @@
1
1
  import { ReactNode } from "react";
2
- import { usePathname, useRouter } from "next/navigation";
3
- import { ApiClient } from "../api/ApiClient";
4
- import { StoreClient } from "../store/StoreClient";
2
+ import { useRouter } from "next/navigation";
3
+ import { StoreManager } from "../store/StoreManager";
5
4
  export declare abstract class Controller<Params = {}> {
6
- router: ReturnType<typeof useRouter>;
7
- pathname: ReturnType<typeof usePathname>;
5
+ protected storeManager: StoreManager;
8
6
  params: Params & {
9
7
  children?: ReactNode;
10
8
  };
11
- store: Record<string, ApiClient | StoreClient>;
9
+ router: ReturnType<typeof useRouter>;
10
+ pathname: string | null;
11
+ constructor(storeManager: StoreManager, params: Params & {
12
+ children?: ReactNode;
13
+ });
14
+ protected getStore(name: string): import("..").StoreClient | import("..").ApiClient;
12
15
  onInit(): void;
13
16
  onRender(): void;
14
17
  }
@@ -2,10 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Controller = void 0;
4
4
  class Controller {
5
+ storeManager;
6
+ params = {};
5
7
  router;
6
8
  pathname;
7
- params = {};
8
- store = {};
9
+ constructor(storeManager, params) {
10
+ this.storeManager = storeManager;
11
+ this.params = params;
12
+ this.onInit();
13
+ }
14
+ getStore(name) {
15
+ return this.storeManager.getStore(name);
16
+ }
9
17
  onInit() { }
10
18
  onRender() { }
11
19
  }
@@ -1,11 +1,5 @@
1
1
  import { Controller } from "./Controller";
2
- export type ControllerClass<C extends Controller> = new () => C;
2
+ import storeInstance from "../store/StoreManager";
3
+ export type ControllerClass<C extends Controller> = new (storeManager: typeof storeInstance, params: Record<string, any>) => C;
3
4
  export type ViewFn<C extends Controller> = (controller: C) => React.ReactNode;
4
- /**
5
- * Crea un componente React uniendo stores, controller y view.
6
- *
7
- * @param stores - Nombres de los stores a los que se suscribe el componente.
8
- * @param ControllerClass - Clase del controller (se instancia una vez al montar).
9
- * @param view - Función que recibe la instancia del controller y retorna JSX.
10
- */
11
5
  export declare function createComponent<C extends Controller>(stores: string[], ControllerClass: ControllerClass<C>, view: ViewFn<C>): (params?: Record<string, any>) => React.ReactNode;
@@ -7,33 +7,17 @@ exports.createComponent = createComponent;
7
7
  const react_1 = require("react");
8
8
  const navigation_1 = require("next/navigation");
9
9
  const StoreManager_1 = __importDefault(require("../store/StoreManager"));
10
- /**
11
- * Crea un componente React uniendo stores, controller y view.
12
- *
13
- * @param stores - Nombres de los stores a los que se suscribe el componente.
14
- * @param ControllerClass - Clase del controller (se instancia una vez al montar).
15
- * @param view - Función que recibe la instancia del controller y retorna JSX.
16
- */
17
10
  function createComponent(stores, ControllerClass, view) {
18
11
  return function (params = {}) {
19
- // Suscribe reactivamente a cada store declarado — Zustand re-renderiza cuando cambian
20
12
  stores.forEach((name) => StoreManager_1.default.getStore(name)?.use());
21
- // Instancia el controller una sola vez por montaje
22
- const ref = (0, react_1.useRef)(null);
23
- if (!ref.current) {
24
- ref.current = new ControllerClass();
25
- ref.current.onInit();
26
- }
13
+ const router = (0, navigation_1.useRouter)();
14
+ const pathname = (0, navigation_1.usePathname)();
15
+ const ref = (0, react_1.useRef)(undefined);
16
+ if (!ref.current)
17
+ ref.current = new ControllerClass(StoreManager_1.default, params);
27
18
  const c = ref.current;
28
- // Inyecta hooks de Next.js y params en cada render
29
- c.router = (0, navigation_1.useRouter)();
30
- c.pathname = (0, navigation_1.usePathname)();
31
- c.params = params;
32
- // Inyecta el mapa de stores en cada render para que los métodos lean valores frescos
33
- stores.forEach((name) => {
34
- c.store[name] = StoreManager_1.default.getStore(name);
35
- });
36
- // Ejecuta onRender en cada render
19
+ c.router = router;
20
+ c.pathname = pathname;
37
21
  c.onRender();
38
22
  return view(c);
39
23
  };
@@ -0,0 +1,19 @@
1
+ type Filter = {
2
+ op: string;
3
+ value: any;
4
+ };
5
+ export type Filters = {
6
+ and?: boolean;
7
+ filters?: Record<string, Filter>;
8
+ };
9
+ export type QueryParams = {
10
+ skip?: number;
11
+ top?: number;
12
+ filters?: Filters;
13
+ expand?: string;
14
+ orderby?: string;
15
+ };
16
+ export declare class QueryBuilder {
17
+ static build(path: string, params?: QueryParams): string;
18
+ }
19
+ export {};
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryBuilder = void 0;
4
+ function buildFilters(filters) {
5
+ if (!filters)
6
+ return "";
7
+ return Object.entries(filters.filters ?? {})
8
+ .map(([key, { op, value }]) => `${key} ${op} ${value}`)
9
+ .join(filters.and ? " and " : " or ");
10
+ }
11
+ class QueryBuilder {
12
+ static build(path, params = {}) {
13
+ const { skip, filters, expand, orderby, top } = params;
14
+ const queryParams = [];
15
+ const filtersQuery = buildFilters(filters);
16
+ if (filtersQuery)
17
+ queryParams.push(`filter=${filtersQuery}`);
18
+ if (expand)
19
+ queryParams.push(`expand=${expand}`);
20
+ if (orderby)
21
+ queryParams.push(`orderby=${orderby}`);
22
+ if (top != null)
23
+ queryParams.push(`top=${top}`);
24
+ if (skip != null)
25
+ queryParams.push(`skip=${skip}`);
26
+ return queryParams.length ? `${path}?${queryParams.join("&")}` : path;
27
+ }
28
+ }
29
+ exports.QueryBuilder = QueryBuilder;
package/package.json CHANGED
@@ -1,40 +1,39 @@
1
1
  {
2
2
  "name": "@nano-app/frontend",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "View + Controller frontend architecture for React/Next.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
+ "import": "./dist/index.js",
9
10
  "require": "./dist/index.js",
10
11
  "types": "./dist/index.d.ts"
11
- },
12
- "./plugin": {
13
- "require": "./dist/plugin/index.js",
14
- "types": "./dist/plugin/index.d.ts"
15
12
  }
16
13
  },
17
- "files": ["dist"],
14
+ "files": [
15
+ "dist"
16
+ ],
18
17
  "scripts": {
19
18
  "build": "tsc",
20
- "prepublishOnly": "npm run build"
19
+ "publish": "npm run build && npm publish --access public"
21
20
  },
22
- "keywords": ["architecture", "typescript", "react", "next", "controller", "view"],
21
+ "keywords": [
22
+ "architecture",
23
+ "typescript",
24
+ "react",
25
+ "next",
26
+ "controller",
27
+ "view"
28
+ ],
23
29
  "author": "Cristian Serrano",
24
30
  "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/Nano-Nathan/nano-app.git"
28
- },
29
31
  "peerDependencies": {
32
+ "@nano-app/backend": "file:../backend",
30
33
  "next": ">=14.0.0",
31
34
  "react": ">=18.0.0",
32
35
  "react-dom": ">=18.0.0",
33
- "zustand": ">=5.0.0",
34
- "@nano-app/backend": "file:../backend"
35
- },
36
- "dependencies": {
37
- "jiti": "^1.21.0"
36
+ "zustand": ">=5.0.0"
38
37
  },
39
38
  "devDependencies": {
40
39
  "@types/node": "^20.0.0",
@@ -44,5 +43,8 @@
44
43
  "react-dom": "^18.0.0",
45
44
  "typescript": "^5.3.0",
46
45
  "zustand": "^5.0.0"
46
+ },
47
+ "dependencies": {
48
+ "fs": "^0.0.1-security"
47
49
  }
48
50
  }