@airoom/nextmin-react 1.4.5 → 2.0.1
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 +29 -3
- package/dist/auth/SignInForm.js +4 -2
- package/dist/components/AdminApp.js +15 -38
- package/dist/components/ArchitectureDemo.d.ts +1 -0
- package/dist/components/ArchitectureDemo.js +45 -0
- package/dist/components/PhoneInput.d.ts +3 -0
- package/dist/components/PhoneInput.js +23 -19
- package/dist/components/RefSelect.d.ts +16 -0
- package/dist/components/RefSelect.js +225 -0
- package/dist/components/SchemaForm.js +131 -51
- package/dist/components/Sidebar.js +6 -13
- package/dist/components/TableFilters.js +2 -0
- package/dist/components/editor/TiptapEditor.js +1 -1
- package/dist/components/editor/Toolbar.js +13 -2
- package/dist/components/editor/components/DistrictGridModal.js +2 -3
- package/dist/components/editor/components/SchemaInsertionModal.js +2 -2
- package/dist/components/viewer/DynamicViewer.js +70 -9
- package/dist/hooks/useRealtime.d.ts +8 -0
- package/dist/hooks/useRealtime.js +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/AuthClient.d.ts +15 -0
- package/dist/lib/AuthClient.js +63 -0
- package/dist/lib/QueryBuilder.d.ts +29 -0
- package/dist/lib/QueryBuilder.js +74 -0
- package/dist/lib/RealtimeClient.d.ts +16 -0
- package/dist/lib/RealtimeClient.js +56 -0
- package/dist/lib/api.d.ts +15 -3
- package/dist/lib/api.js +71 -58
- package/dist/lib/auth.js +7 -2
- package/dist/lib/types.d.ts +16 -0
- package/dist/nextmin.css +1 -1
- package/dist/providers/NextMinProvider.d.ts +8 -1
- package/dist/providers/NextMinProvider.js +40 -8
- package/dist/router/NextMinRouter.d.ts +1 -1
- package/dist/router/NextMinRouter.js +1 -1
- package/dist/state/schemasSlice.js +8 -2
- package/dist/views/DashboardPage.js +56 -42
- package/dist/views/ListPage.js +34 -4
- package/dist/views/SettingsEdit.js +25 -2
- package/dist/views/list/DataTableHero.js +103 -46
- package/dist/views/list/ListHeader.d.ts +3 -1
- package/dist/views/list/ListHeader.js +2 -2
- package/dist/views/list/jsonSummary.d.ts +3 -3
- package/dist/views/list/jsonSummary.js +47 -20
- package/dist/views/list/useListData.js +5 -1
- package/package.json +8 -4
- package/dist/components/RefMultiSelect.d.ts +0 -22
- package/dist/components/RefMultiSelect.js +0 -113
- package/dist/components/RefSingleSelect.d.ts +0 -17
- package/dist/components/RefSingleSelect.js +0 -110
- package/dist/lib/schemaService.d.ts +0 -2
- package/dist/lib/schemaService.js +0 -39
- package/dist/state/schemaLive.d.ts +0 -2
- package/dist/state/schemaLive.js +0 -19
- /package/dist/{editor.css → components/editor/editor.css} +0 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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',
|
package/dist/lib/auth.js
CHANGED
|
@@ -16,6 +16,9 @@ function normalizeRole(value) {
|
|
|
16
16
|
if (typeof value === 'object' && typeof value.name === 'string') {
|
|
17
17
|
return String(value.name).toLowerCase();
|
|
18
18
|
}
|
|
19
|
+
if (typeof value === 'number') {
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
catch { }
|
|
21
24
|
return null;
|
|
@@ -30,8 +33,10 @@ export async function login({ email, password }) {
|
|
|
30
33
|
throw new Error(res?.message || 'Login failed');
|
|
31
34
|
}
|
|
32
35
|
// Enforce admin-only access here (superadmin/admin)
|
|
33
|
-
const
|
|
34
|
-
|
|
36
|
+
const roleName = String(user?.roleName || '').toLowerCase();
|
|
37
|
+
const normalized = normalizeRole(user?.role);
|
|
38
|
+
const isAdmin = roleName === 'admin' || roleName === 'superadmin' || normalized === 'admin' || normalized === 'superadmin';
|
|
39
|
+
if (!isAdmin) {
|
|
35
40
|
throw new Error('You are not authorized to access the admin panel');
|
|
36
41
|
}
|
|
37
42
|
return { token, user };
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -7,9 +7,23 @@ export type AttributeBase = {
|
|
|
7
7
|
longtext?: boolean;
|
|
8
8
|
show?: string;
|
|
9
9
|
populateSlugFrom?: string;
|
|
10
|
+
minLength?: number;
|
|
11
|
+
maxLength?: number;
|
|
12
|
+
pattern?: string;
|
|
13
|
+
mask?: string;
|
|
14
|
+
inherited?: boolean;
|
|
10
15
|
};
|
|
11
16
|
export type ArrayAttribute = AttributeBase[];
|
|
12
17
|
export type Attribute = AttributeBase | ArrayAttribute;
|
|
18
|
+
export type CustomAction = {
|
|
19
|
+
label: string;
|
|
20
|
+
href: string;
|
|
21
|
+
icon?: string;
|
|
22
|
+
variant?: 'light' | 'flat' | 'solid' | 'bordered';
|
|
23
|
+
color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
24
|
+
relatedModel?: string;
|
|
25
|
+
relatedField?: string;
|
|
26
|
+
};
|
|
13
27
|
export type SchemaDef = {
|
|
14
28
|
modelName: string;
|
|
15
29
|
attributes: Record<string, Attribute>;
|
|
@@ -19,6 +33,8 @@ export type SchemaDef = {
|
|
|
19
33
|
update?: boolean;
|
|
20
34
|
delete?: boolean;
|
|
21
35
|
};
|
|
36
|
+
extends?: string;
|
|
37
|
+
actions?: CustomAction[];
|
|
22
38
|
};
|
|
23
39
|
export type SchemasResponse = {
|
|
24
40
|
success: true;
|