@airoom/nextmin-react 1.4.6 → 2.0.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.
Files changed (56) hide show
  1. package/README.md +29 -3
  2. package/dist/auth/SignInForm.js +4 -2
  3. package/dist/components/AdminApp.js +15 -38
  4. package/dist/components/ArchitectureDemo.d.ts +1 -0
  5. package/dist/components/ArchitectureDemo.js +45 -0
  6. package/dist/components/PhoneInput.d.ts +3 -0
  7. package/dist/components/PhoneInput.js +23 -19
  8. package/dist/components/RefSelect.d.ts +16 -0
  9. package/dist/components/RefSelect.js +225 -0
  10. package/dist/components/SchemaForm.js +125 -50
  11. package/dist/components/Sidebar.js +6 -13
  12. package/dist/components/TableFilters.js +2 -0
  13. package/dist/components/editor/TiptapEditor.js +4 -2
  14. package/dist/components/editor/Toolbar.js +21 -2
  15. package/dist/components/editor/components/DistrictGridModal.js +2 -3
  16. package/dist/components/editor/components/SchemaInsertionModal.js +2 -2
  17. package/dist/components/viewer/DynamicViewer.js +70 -9
  18. package/dist/hooks/useRealtime.d.ts +8 -0
  19. package/dist/hooks/useRealtime.js +30 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +6 -0
  22. package/dist/lib/AuthClient.d.ts +15 -0
  23. package/dist/lib/AuthClient.js +63 -0
  24. package/dist/lib/QueryBuilder.d.ts +29 -0
  25. package/dist/lib/QueryBuilder.js +74 -0
  26. package/dist/lib/RealtimeClient.d.ts +16 -0
  27. package/dist/lib/RealtimeClient.js +56 -0
  28. package/dist/lib/api.d.ts +15 -3
  29. package/dist/lib/api.js +71 -58
  30. package/dist/lib/auth.js +7 -2
  31. package/dist/lib/types.d.ts +16 -0
  32. package/dist/nextmin.css +1 -1
  33. package/dist/providers/NextMinProvider.d.ts +8 -1
  34. package/dist/providers/NextMinProvider.js +40 -8
  35. package/dist/router/NextMinRouter.d.ts +1 -1
  36. package/dist/router/NextMinRouter.js +1 -1
  37. package/dist/state/schemasSlice.js +4 -27
  38. package/dist/views/DashboardPage.js +56 -42
  39. package/dist/views/ListPage.js +34 -4
  40. package/dist/views/SettingsEdit.js +25 -2
  41. package/dist/views/list/DataTableHero.js +103 -46
  42. package/dist/views/list/ListHeader.d.ts +3 -1
  43. package/dist/views/list/ListHeader.js +2 -2
  44. package/dist/views/list/jsonSummary.d.ts +3 -3
  45. package/dist/views/list/jsonSummary.js +47 -20
  46. package/dist/views/list/useListData.js +5 -1
  47. package/package.json +9 -4
  48. package/dist/components/RefMultiSelect.d.ts +0 -22
  49. package/dist/components/RefMultiSelect.js +0 -113
  50. package/dist/components/RefSingleSelect.d.ts +0 -17
  51. package/dist/components/RefSingleSelect.js +0 -110
  52. package/dist/lib/schemaService.d.ts +0 -2
  53. package/dist/lib/schemaService.js +0 -39
  54. package/dist/state/schemaLive.d.ts +0 -2
  55. package/dist/state/schemaLive.js +0 -19
  56. /package/dist/{editor.css → components/editor/editor.css} +0 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from 'react';
4
+ import DOMPurify from 'dompurify';
4
5
  import { formatCell } from '../../views/list/formatters';
5
6
  import { summarizeObject } from '../../views/list/jsonSummary';
6
7
  // Hide technical keys in the viewer
@@ -32,6 +33,7 @@ function isPasswordByAttr(attr) {
32
33
  }
33
34
  // --- small helpers (duplicated minimal logic from table) ---
34
35
  const IMG_EXT_RE = /(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
36
+ const HTML_TAG_RE = /<([a-z1-6]+)(?:\s+[^>]*?)?>[\s\S]*?<\/\1>|<[a-z1-6]+(?:\s+[^>]*?)?\s*\/>|&[a-z0-9#]+;/i;
35
37
  function isUrlLike(s) {
36
38
  return /^https?:\/\//i.test(s) || s.startsWith('/') || s.startsWith('data:');
37
39
  }
@@ -118,6 +120,34 @@ function formatDateTimeLocal(d) {
118
120
  return d.toString();
119
121
  }
120
122
  }
123
+ /** Detect HH:mm or HH:mm:ss and format to localized time */
124
+ function formatTimeLocal(val) {
125
+ if (typeof val !== 'string')
126
+ return null;
127
+ const s = val.trim();
128
+ // Match HH:mm or HH:mm:ss (24h format)
129
+ const timeMatch = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
130
+ if (!timeMatch)
131
+ return null;
132
+ try {
133
+ const hours = parseInt(timeMatch[1], 10);
134
+ const minutes = parseInt(timeMatch[2], 10);
135
+ const seconds = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0;
136
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
137
+ return null;
138
+ }
139
+ const d = new Date();
140
+ d.setHours(hours, minutes, seconds);
141
+ return d.toLocaleTimeString(undefined, {
142
+ hour: '2-digit',
143
+ minute: '2-digit',
144
+ hour12: true,
145
+ });
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
121
151
  function isPrivate(attr) {
122
152
  const a = (Array.isArray(attr) ? attr[0] : attr);
123
153
  return !!a?.private;
@@ -152,12 +182,23 @@ function toLabel(v) {
152
182
  }
153
183
  return null;
154
184
  }
185
+ function isBadLabel(s) {
186
+ if (!s)
187
+ return true;
188
+ if (s.length > 50)
189
+ return true;
190
+ if (s.startsWith('http'))
191
+ return true;
192
+ if (/^[a-fA-F0-9]{24}$/.test(s))
193
+ return true;
194
+ return false;
195
+ }
155
196
  function listLabels(arr) {
156
197
  if (!Array.isArray(arr))
157
198
  return null;
158
199
  const labels = arr
159
200
  .map((it) => toLabel(it))
160
- .filter((s) => !!s && !!s.trim());
201
+ .filter((s) => !!s && !!s.trim() && !isBadLabel(s));
161
202
  return labels.length ? labels : null;
162
203
  }
163
204
  function summarizeList(labels, max = 10) {
@@ -210,10 +251,17 @@ function maybeParseJson(val) {
210
251
  return val;
211
252
  }
212
253
  }
213
- function renderValue(input, key) {
254
+ function renderValue(input, key, attr) {
214
255
  const val = maybeParseJson(input);
215
256
  if (val == null)
216
257
  return _jsx("span", { className: "text-foreground/50", children: "\u2014" });
258
+ // Detect rich text (explicit or heuristic)
259
+ const a = Array.isArray(attr) ? attr[0] : attr;
260
+ const isRich = a?.rich === true;
261
+ const looksLikeHtml = typeof val === 'string' && HTML_TAG_RE.test(val);
262
+ if ((isRich || looksLikeHtml) && typeof val === 'string' && val.length > 3) {
263
+ return (_jsx("div", { className: "prose prose-sm max-w-none dark:prose-invert", dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(val) } }));
264
+ }
217
265
  // Only parse dates for known date fields (createdAt, updatedAt, deletedAt)
218
266
  if (isDateField(key)) {
219
267
  const d = tryParseDate(val);
@@ -222,38 +270,51 @@ function renderValue(input, key) {
222
270
  return (_jsx("time", { dateTime: iso, title: iso, className: "whitespace-pre-wrap", children: formatDateTimeLocal(d) }));
223
271
  }
224
272
  }
273
+ // Detect and format plain time strings (e.g. "14:00" -> "2:00 PM")
274
+ const localizedTime = formatTimeLocal(val);
275
+ if (localizedTime)
276
+ return _jsx("span", { children: localizedTime });
225
277
  // Detect images
226
278
  const urls = extractAllImageUrls(val);
227
279
  if (urls.length)
228
280
  return _jsx(ImageThumbs, { urls: urls });
229
281
  // Primitive or simple object/array formatting via shared formatter
230
282
  if (Array.isArray(val)) {
283
+ const head = Array.isArray(attr) ? attr[0] : attr;
284
+ const showKey = head?.show;
231
285
  // Prefer shared summarizer for arrays
232
286
  if (val.length && typeof val[0] === 'object') {
233
287
  // Show each summarized object on its own line for readability
234
288
  const lines = val
235
- .map((v) => (v && typeof v === 'object' ? summarizeObject(v)?.text : null))
289
+ .map((v) => (v && typeof v === 'object' ? summarizeObject(v, showKey)?.text : null))
236
290
  .filter((s) => !!s && !!s.trim());
237
291
  if (lines.length) {
238
292
  return (_jsx("div", { className: "grid gap-1", children: lines.map((s, i) => (_jsx("div", { className: "text-foreground/90", children: s }, i))) }));
239
293
  }
240
294
  }
241
295
  // Fallback detailed rendering
242
- return (_jsx("div", { className: "grid gap-2", children: val.map((v, i) => (_jsx("div", { className: "rounded-medium border border-default-100 p-2", children: typeof v === 'object' && v !== null ? (_jsx(ObjectView, { obj: v })) : (_jsx("span", { children: formatCell(v, key) })) }, i))) }));
296
+ return (_jsx("div", { className: "grid gap-2", children: val.map((v, i) => {
297
+ const head = Array.isArray(attr) ? attr[0] : attr;
298
+ const subAttributes = head?.attributes;
299
+ const subShowKey = head?.show;
300
+ return (_jsx("div", { className: "rounded-medium border border-default-100 p-2", children: typeof v === 'object' && v !== null ? (_jsx(ObjectView, { obj: v, attributes: subAttributes, showKey: subShowKey })) : (_jsx("span", { children: formatCell(v, key) })) }, i));
301
+ }) }));
243
302
  }
244
303
  if (typeof val === 'object') {
245
- const condensed = summarizeObject(val)?.text || tryCondenseEntity(val);
304
+ const showKey = (Array.isArray(attr) ? attr[0] : attr)?.show;
305
+ const condensed = summarizeObject(val, showKey)?.text || tryCondenseEntity(val);
246
306
  if (condensed)
247
307
  return _jsx("span", { children: condensed });
248
- return _jsx(ObjectView, { obj: val });
308
+ const head = Array.isArray(attr) ? attr[0] : attr;
309
+ return _jsx(ObjectView, { obj: val, attributes: head?.attributes, showKey: showKey });
249
310
  }
250
311
  return _jsx("span", { children: formatCell(val, key) });
251
312
  }
252
- function ObjectView({ obj }) {
313
+ function ObjectView({ obj, attributes, showKey }) {
253
314
  const entries = Object.entries(obj).filter(([k]) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
254
315
  if (!entries.length)
255
316
  return _jsx("span", { className: "text-foreground/50", children: '{ }' });
256
- return (_jsx("div", { className: "grid gap-1", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "min-w-24 text-foreground/60 text-sm", children: [humanizeLabel(k), ":"] }), _jsx("div", { className: "flex-1", children: renderValue(v, k) })] }, k))) }));
317
+ return (_jsx("div", { className: "grid gap-1", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "min-w-24 text-foreground/60 text-sm", children: [humanizeLabel(k), ":"] }), _jsx("div", { className: "flex-1", children: renderValue(v, k, attributes?.[k]) })] }, k))) }));
257
318
  }
258
319
  export function DynamicViewer({ schema, data }) {
259
320
  // Use schema order if available, otherwise object keys order
@@ -270,5 +331,5 @@ export function DynamicViewer({ schema, data }) {
270
331
  return base;
271
332
  return Object.keys(data || {}).filter((k) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
272
333
  }, [attributes, data]);
273
- return (_jsx("div", { className: "grid gap-1", children: keys.map((key) => (_jsx(FieldRow, { label: humanizeLabel(key), children: renderValue(data?.[key], key) }, key))) }));
334
+ return (_jsx("div", { className: "grid gap-1", children: keys.map((key) => (_jsx(FieldRow, { label: humanizeLabel(key), children: renderValue(data?.[key], key, attributes[key]) }, key))) }));
274
335
  }
@@ -0,0 +1,8 @@
1
+ export interface RealtimeEvent {
2
+ event: string;
3
+ payload: any;
4
+ }
5
+ export declare function useRealtime(): {
6
+ isConnected: boolean;
7
+ lastEvent: RealtimeEvent | null;
8
+ };
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { RealtimeClient } from '../lib/RealtimeClient';
4
+ export function useRealtime() {
5
+ const [isConnected, setIsConnected] = useState(false);
6
+ const [lastEvent, setLastEvent] = useState(null);
7
+ useEffect(() => {
8
+ const rt = RealtimeClient.getInstance();
9
+ // Check initial connection
10
+ setIsConnected(Boolean(rt.socket?.connected));
11
+ const onConnect = () => setIsConnected(true);
12
+ const onDisconnect = () => setIsConnected(false);
13
+ const handleAnyEvent = (event, payload) => {
14
+ setLastEvent({ event, payload });
15
+ };
16
+ const onAnyEvent = (event, ...args) => {
17
+ // We only care about events with a payload (usually args[0])
18
+ setLastEvent({ event, payload: args[0] });
19
+ };
20
+ rt.on('connect', onConnect);
21
+ rt.on('disconnect', onDisconnect);
22
+ rt.onAny(onAnyEvent);
23
+ return () => {
24
+ rt.off('connect', onConnect);
25
+ rt.off('disconnect', onDisconnect);
26
+ rt.offAny(onAnyEvent);
27
+ };
28
+ }, []);
29
+ return { isConnected, lastEvent };
30
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  export * from './providers/NextMinProvider';
2
2
  export * from './components/AdminApp';
3
+ export * from './components/ArchitectureDemo';
4
+ export * from './hooks/useRealtime';
5
+ export * from './lib/QueryBuilder';
6
+ export * from './lib/AuthClient';
7
+ export * from './lib/RealtimeClient';
8
+ export * from './lib/api';
package/dist/index.js CHANGED
@@ -1,2 +1,8 @@
1
1
  export * from './providers/NextMinProvider';
2
2
  export * from './components/AdminApp';
3
+ export * from './components/ArchitectureDemo';
4
+ export * from './hooks/useRealtime';
5
+ export * from './lib/QueryBuilder';
6
+ export * from './lib/AuthClient';
7
+ export * from './lib/RealtimeClient';
8
+ export * from './lib/api';
@@ -0,0 +1,15 @@
1
+ export declare class AuthClient {
2
+ private static STORAGE_KEY_TOKEN;
3
+ private static STORAGE_KEY_USER;
4
+ private static baseURL;
5
+ private static apiKey;
6
+ static setBaseURL(url: string): void;
7
+ static getBaseURL(): string;
8
+ static setApiKey(key: string): void;
9
+ static getApiKey(): string;
10
+ static getToken(): string | null;
11
+ static getUser(): any | null;
12
+ static setSession(token: string, user: any): void;
13
+ static clearSession(): void;
14
+ static isValidToken(token: string | null): boolean;
15
+ }
@@ -0,0 +1,63 @@
1
+ export class AuthClient {
2
+ static setBaseURL(url) {
3
+ this.baseURL = url;
4
+ }
5
+ static getBaseURL() {
6
+ return this.baseURL;
7
+ }
8
+ static setApiKey(key) {
9
+ this.apiKey = key;
10
+ }
11
+ static getApiKey() {
12
+ return this.apiKey;
13
+ }
14
+ static getToken() {
15
+ if (typeof window === 'undefined')
16
+ return null;
17
+ return localStorage.getItem(this.STORAGE_KEY_TOKEN);
18
+ }
19
+ static getUser() {
20
+ if (typeof window === 'undefined')
21
+ return null;
22
+ const user = localStorage.getItem(this.STORAGE_KEY_USER);
23
+ try {
24
+ return user ? JSON.parse(user) : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ static setSession(token, user) {
31
+ if (typeof window === 'undefined')
32
+ return;
33
+ localStorage.setItem(this.STORAGE_KEY_TOKEN, token);
34
+ localStorage.setItem(this.STORAGE_KEY_USER, JSON.stringify(user));
35
+ }
36
+ static clearSession() {
37
+ if (typeof window === 'undefined')
38
+ return;
39
+ localStorage.removeItem(this.STORAGE_KEY_TOKEN);
40
+ localStorage.removeItem(this.STORAGE_KEY_USER);
41
+ }
42
+ static isValidToken(token) {
43
+ if (!token)
44
+ return false;
45
+ const parts = token.split('.');
46
+ if (parts.length !== 3)
47
+ return true; // non-JWT token
48
+ try {
49
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
50
+ if (typeof payload?.exp === 'number') {
51
+ return payload.exp * 1000 > Date.now();
52
+ }
53
+ return true;
54
+ }
55
+ catch {
56
+ return true;
57
+ }
58
+ }
59
+ }
60
+ AuthClient.STORAGE_KEY_TOKEN = 'nextmin.token';
61
+ AuthClient.STORAGE_KEY_USER = 'nextmin.user';
62
+ AuthClient.baseURL = '/api';
63
+ AuthClient.apiKey = '';
@@ -0,0 +1,29 @@
1
+ export type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith';
2
+ export interface Filter {
3
+ field: string;
4
+ operator: FilterOperator;
5
+ value: any;
6
+ }
7
+ export interface QueryOptions {
8
+ where?: Record<string, any> | Filter[];
9
+ sort?: string;
10
+ sortType?: string;
11
+ page?: number;
12
+ limit?: number;
13
+ fields?: string[];
14
+ q?: string;
15
+ searchKeys?: string;
16
+ }
17
+ /**
18
+ * QueryBuilder helps construct complex filter queries for the NextMin API.
19
+ */
20
+ export declare class QueryBuilder {
21
+ private options;
22
+ where(field: string, operator: FilterOperator, value: any): QueryBuilder;
23
+ orWhere(field: string, operator: FilterOperator, value: any): QueryBuilder;
24
+ sort(field: string, direction?: 'asc' | 'desc'): QueryBuilder;
25
+ page(page: number, limit?: number): QueryBuilder;
26
+ limit(limit: number): QueryBuilder;
27
+ search(q: string, keys?: string[]): QueryBuilder;
28
+ build(): Record<string, string>;
29
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * QueryBuilder helps construct complex filter queries for the NextMin API.
3
+ */
4
+ export class QueryBuilder {
5
+ constructor() {
6
+ this.options = {};
7
+ }
8
+ where(field, operator, value) {
9
+ if (!this.options.where)
10
+ this.options.where = {};
11
+ const map = {
12
+ eq: '$eq', ne: '$ne', gt: '$gt', gte: '$gte', lt: '$lt', lte: '$lte',
13
+ in: '$in', nin: '$nin', contains: '$regex', startsWith: '$regex', endsWith: '$regex'
14
+ };
15
+ let actualValue = value;
16
+ if (operator === 'contains')
17
+ actualValue = { $regex: value, $options: 'i' };
18
+ else if (operator === 'startsWith')
19
+ actualValue = { $regex: `^${value}`, $options: 'i' };
20
+ else if (operator === 'endsWith')
21
+ actualValue = { $regex: `${value}$`, $options: 'i' };
22
+ else
23
+ actualValue = { [map[operator]]: value };
24
+ this.options.where[field] = actualValue;
25
+ return this;
26
+ }
27
+ orWhere(field, operator, value) {
28
+ if (!this.options.where)
29
+ this.options.where = {};
30
+ // This is a simplified OR for the pilot. Complex nested ORs would need a different IR.
31
+ const currentWhere = this.options.where;
32
+ const newCondition = this.where(field, operator, value).options.where;
33
+ this.options.where = { $or: [currentWhere, newCondition] };
34
+ return this;
35
+ }
36
+ sort(field, direction = 'asc') {
37
+ this.options.sort = field;
38
+ this.options.sortType = direction;
39
+ return this;
40
+ }
41
+ page(page, limit = 10) {
42
+ this.options.page = page;
43
+ this.options.limit = limit;
44
+ return this;
45
+ }
46
+ limit(limit) {
47
+ this.options.limit = limit;
48
+ return this;
49
+ }
50
+ search(q, keys) {
51
+ this.options.q = q;
52
+ if (keys)
53
+ this.options.searchKeys = keys.join(',');
54
+ return this;
55
+ }
56
+ build() {
57
+ const params = {};
58
+ if (this.options.where)
59
+ params.where = JSON.stringify(this.options.where);
60
+ if (this.options.sort)
61
+ params.sort = this.options.sort;
62
+ if (this.options.sortType)
63
+ params.sortType = this.options.sortType;
64
+ if (this.options.page !== undefined)
65
+ params.page = String(this.options.page);
66
+ if (this.options.limit !== undefined)
67
+ params.limit = String(this.options.limit);
68
+ if (this.options.q)
69
+ params.q = this.options.q;
70
+ if (this.options.searchKeys)
71
+ params.searchKeys = this.options.searchKeys;
72
+ return params;
73
+ }
74
+ }
@@ -0,0 +1,16 @@
1
+ export declare class RealtimeClient {
2
+ private socket;
3
+ private static instance;
4
+ private baseURL;
5
+ apiKey: string;
6
+ private constructor();
7
+ static getInstance(): RealtimeClient;
8
+ setBaseURL(url: string): void;
9
+ setApiKey(key: string): void;
10
+ connect(): void;
11
+ on(event: string, callback: (data: any) => void): void;
12
+ onAny(callback: (event: string, ...args: any[]) => void): void;
13
+ off(event: string, callback: (data: any) => void): void;
14
+ offAny(callback: (event: string, ...args: any[]) => void): void;
15
+ disconnect(): void;
16
+ }
@@ -0,0 +1,56 @@
1
+ import { io } from 'socket.io-client';
2
+ import { AuthClient } from './AuthClient';
3
+ const RAW_API = process.env.NEXT_PUBLIC_NEXTMIN_API_URL ?? 'http://localhost:8081';
4
+ const SOCKET_URL = RAW_API.replace('/rest', ''); // Assume socket is at root or similar path
5
+ export class RealtimeClient {
6
+ constructor() {
7
+ this.socket = null;
8
+ this.baseURL = RAW_API.replace('/rest', '');
9
+ this.apiKey = process.env.NEXT_PUBLIC_NEXTMIN_API_KEY ?? '';
10
+ }
11
+ static getInstance() {
12
+ if (!RealtimeClient.instance) {
13
+ RealtimeClient.instance = new RealtimeClient();
14
+ }
15
+ return RealtimeClient.instance;
16
+ }
17
+ setBaseURL(url) {
18
+ this.baseURL = url;
19
+ }
20
+ setApiKey(key) {
21
+ this.apiKey = key;
22
+ }
23
+ connect() {
24
+ if (this.socket?.connected)
25
+ return;
26
+ this.socket = io(`${this.baseURL}/realtime`, {
27
+ path: '/__nextmin__/realtime',
28
+ auth: {
29
+ apiKey: this.apiKey,
30
+ token: AuthClient.getToken()
31
+ }
32
+ });
33
+ this.socket.on('connect', () => {
34
+ console.log('[NextMin] Realtime connected');
35
+ });
36
+ this.socket.on('connect_error', (err) => {
37
+ console.warn('[NextMin] Realtime connection error:', err.message);
38
+ });
39
+ }
40
+ on(event, callback) {
41
+ this.socket?.on(event, callback);
42
+ }
43
+ onAny(callback) {
44
+ this.socket?.onAny(callback);
45
+ }
46
+ off(event, callback) {
47
+ this.socket?.off(event, callback);
48
+ }
49
+ offAny(callback) {
50
+ this.socket?.offAny(callback);
51
+ }
52
+ disconnect() {
53
+ this.socket?.disconnect();
54
+ this.socket = null;
55
+ }
56
+ }
package/dist/lib/api.d.ts CHANGED
@@ -1,18 +1,29 @@
1
1
  import { ApiItemResponse, ApiListResponse } from './types';
2
+ import { QueryOptions } from './QueryBuilder';
3
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
2
4
  export declare class ApiError extends Error {
3
5
  status: number;
4
6
  info?: unknown;
5
7
  constructor(message: string, status: number, info?: unknown);
6
8
  }
7
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+ export declare class NextMinClient {
10
+ private baseURL;
11
+ private apiKey;
12
+ constructor(options?: {
13
+ baseURL?: string;
14
+ apiKey?: string;
15
+ });
16
+ request<T>(path: string, opts?: FetchOpts): Promise<T>;
17
+ }
18
+ declare function request<T>(path: string, opts?: FetchOpts): Promise<T>;
8
19
  type FetchOpts = {
9
20
  method?: HttpMethod;
10
21
  body?: unknown;
11
22
  token?: string | null;
12
23
  json?: boolean;
13
24
  auth?: boolean;
25
+ query?: Record<string, string>;
14
26
  };
15
- declare function request<T>(path: string, opts?: FetchOpts): Promise<T>;
16
27
  export type ListParams = {
17
28
  q?: string;
18
29
  searchKey?: string;
@@ -22,7 +33,8 @@ export type ListParams = {
22
33
  };
23
34
  export declare const api: {
24
35
  getSchemas: () => Promise<unknown>;
25
- list: <T>(modelName: string, page?: number, limit?: number, params?: ListParams) => Promise<ApiListResponse<T>>;
36
+ cleanup: () => Promise<unknown>;
37
+ list: <T>(modelName: string, options?: QueryOptions) => Promise<ApiListResponse<T>>;
26
38
  get: <T>(modelName: string, id: string) => Promise<ApiItemResponse<T>>;
27
39
  create: <T>(modelName: string, data: unknown) => Promise<ApiItemResponse<T>>;
28
40
  update: <T>(modelName: string, id: string, data: unknown) => Promise<ApiItemResponse<T>>;
package/dist/lib/api.js CHANGED
@@ -1,9 +1,6 @@
1
- const RAW_API = process.env.NEXT_PUBLIC_NEXTMIN_API_URL ?? 'http://localhost:8081/rest';
2
- const API_KEY = process.env.NEXT_PUBLIC_NEXTMIN_API_KEY ?? '';
3
- function normalizeBase(base) {
4
- return base.replace(/\/+$/, '');
5
- }
6
- const API_BASE = normalizeBase(RAW_API);
1
+ import { AuthClient } from './AuthClient';
2
+ import { QueryBuilder } from './QueryBuilder';
3
+ const normalizeBase = (base) => base.replace(/\/+$/, '');
7
4
  export class ApiError extends Error {
8
5
  constructor(message, status, info) {
9
6
  super(message);
@@ -12,63 +9,56 @@ export class ApiError extends Error {
12
9
  this.info = info;
13
10
  }
14
11
  }
15
- function tokenFromStorage() {
16
- try {
17
- if (typeof window === 'undefined')
18
- return null;
19
- return localStorage.getItem('nextmin.token');
12
+ export class NextMinClient {
13
+ constructor(options = {}) {
14
+ this.baseURL = options.baseURL ? normalizeBase(options.baseURL) : normalizeBase(AuthClient.getBaseURL());
15
+ this.apiKey = options.apiKey ?? AuthClient.getApiKey();
20
16
  }
21
- catch {
22
- return null;
17
+ async request(path, opts = {}) {
18
+ const { method = 'GET', body, token, json = true, auth = true, query } = opts;
19
+ const headers = { 'x-api-key': this.apiKey || AuthClient.getApiKey() };
20
+ const isForm = typeof FormData !== 'undefined' && body instanceof FormData;
21
+ if (json && !isForm)
22
+ headers['Content-Type'] = 'application/json';
23
+ const bearer = token ?? AuthClient.getToken();
24
+ if (auth && bearer)
25
+ headers.Authorization = `Bearer ${bearer}`;
26
+ const queryString = query ? qs(query) : '';
27
+ const url = `${this.baseURL || normalizeBase(AuthClient.getBaseURL())}${path}${queryString}`;
28
+ let res;
29
+ try {
30
+ res = await fetch(url, {
31
+ method,
32
+ headers,
33
+ body: body && json && !isForm ? JSON.stringify(body) : body,
34
+ });
35
+ }
36
+ catch (networkErr) {
37
+ const err = new ApiError(networkErr?.message || 'Network error', 0);
38
+ err.request = { url, method, headers };
39
+ throw err;
40
+ }
41
+ const raw = await res.text();
42
+ let payload = {};
43
+ try {
44
+ payload = raw ? JSON.parse(raw) : {};
45
+ }
46
+ catch {
47
+ payload = raw || {};
48
+ }
49
+ if (!res.ok) {
50
+ throw payload;
51
+ }
52
+ return payload;
23
53
  }
24
54
  }
55
+ // Keep the global request for backward compatibility, but make it use AuthClient config
56
+ async function request(path, opts = {}) {
57
+ return new NextMinClient().request(path, opts);
58
+ }
25
59
  function modelPath(modelName) {
26
60
  return modelName.toLowerCase() === 'user' ? '/auth/user' : `/${modelName}`;
27
61
  }
28
- async function request(path, opts = {}) {
29
- const { method = 'GET', body, token, json = true, auth = true } = opts;
30
- const headers = { 'x-api-key': API_KEY };
31
- // Don't force JSON when sending FormData/Blob
32
- const isForm = typeof FormData !== 'undefined' && body instanceof FormData;
33
- if (json && !isForm)
34
- headers['Content-Type'] = 'application/json';
35
- const bearer = token ?? tokenFromStorage();
36
- if (auth && bearer)
37
- headers.Authorization = `Bearer ${bearer}`;
38
- let res;
39
- try {
40
- res = await fetch(`${API_BASE}${path}`, {
41
- method,
42
- headers,
43
- body: body && json && !isForm
44
- ? JSON.stringify(body)
45
- : body,
46
- });
47
- }
48
- catch (networkErr) {
49
- // Network/DNS/CORS timeouts etc.
50
- const err = new ApiError(networkErr?.message || 'Network error', 0);
51
- err.request = { url: `${API_BASE}${path}`, method, headers };
52
- throw err;
53
- }
54
- const raw = await res.text();
55
- let payload = {};
56
- try {
57
- payload = raw ? JSON.parse(raw) : {};
58
- }
59
- catch {
60
- payload = raw || {};
61
- }
62
- if (!res.ok) {
63
- const status = res.status;
64
- const serverMsg = payload?.message ??
65
- payload?.error ??
66
- res.statusText ??
67
- 'API error';
68
- throw payload;
69
- }
70
- return payload;
71
- }
72
62
  function qs(params) {
73
63
  const sp = new URLSearchParams();
74
64
  for (const [k, v] of Object.entries(params)) {
@@ -84,7 +74,30 @@ function qs(params) {
84
74
  }
85
75
  export const api = {
86
76
  getSchemas: () => request('/_schemas'),
87
- list: (modelName, page = 0, limit = 10, params = {}) => request(`${modelPath(modelName)}${qs({ page, limit, ...params })}`),
77
+ cleanup: () => request('/_cleanup', { method: 'POST' }),
78
+ list: (modelName, options = {}) => {
79
+ const builder = new QueryBuilder();
80
+ if (options.where) {
81
+ if (Array.isArray(options.where)) {
82
+ options.where.forEach(f => builder.where(f.field, f.operator, f.value));
83
+ }
84
+ else {
85
+ // Assume it's already a transformed object or handle it
86
+ Object.keys(options.where).forEach(k => {
87
+ // This is a bit simplified, but compatible with the builder's internal state
88
+ builder.options.where = options.where;
89
+ });
90
+ }
91
+ }
92
+ if (options.sort)
93
+ builder.sort(options.sort, options.sortType);
94
+ if (options.page !== undefined)
95
+ builder.page(Math.max(1, options.page), options.limit);
96
+ if (options.q)
97
+ builder.search(options.q, options.searchKeys?.split(','));
98
+ const params = builder.build();
99
+ return request(`${modelPath(modelName)}${qs(params)}`);
100
+ },
88
101
  get: (modelName, id) => request(`${modelPath(modelName)}/${id}`),
89
102
  create: (modelName, data) => request(modelPath(modelName), {
90
103
  method: 'POST',