@svadmin/core 0.0.6 → 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/package.json +4 -1
- package/src/audit.ts +37 -0
- package/src/auth-hooks.test.ts +205 -0
- package/src/context.ts +65 -15
- package/src/data-transfer.ts +148 -70
- package/src/form-hooks.svelte.ts +266 -0
- package/src/helpers.ts +157 -0
- package/src/hook-utils.svelte.ts +121 -0
- package/src/hooks.svelte.ts +277 -530
- package/src/http-error.test.ts +85 -0
- package/src/i18n.svelte.ts +18 -0
- package/src/index.ts +39 -8
- package/src/inferencer-openapi.test.ts +192 -0
- package/src/inferencer-openapi.ts +290 -0
- package/src/live-sse.ts +146 -0
- package/src/live-websocket.ts +141 -0
- package/src/live.test.ts +134 -0
- package/src/live.ts +60 -8
- package/src/mutation-hooks.svelte.ts +301 -0
- package/src/options.ts +95 -0
- package/src/query-hooks.svelte.ts +213 -0
- package/src/query-keys.ts +22 -0
- package/src/resource-types.test.ts +79 -0
- package/src/router.ts +28 -4
- package/src/routing-hooks.svelte.ts +99 -0
- package/src/table-hooks.svelte.ts +178 -0
- package/src/toast.svelte.ts +8 -4
- package/src/types.ts +124 -47
- package/src/unsaved-changes.svelte.ts +62 -0
- package/src/url-sync.ts +15 -0
- package/src/useParsed.ts +39 -3
- package/src/useStepsForm.svelte.ts +71 -0
- package/src/utility-hooks.svelte.ts +161 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// HttpError + CrudOperator + LogicalFilter tests
|
|
2
|
+
import { describe, test, expect } from 'bun:test';
|
|
3
|
+
import { HttpError } from './types';
|
|
4
|
+
import type { CrudOperator, Filter, LogicalFilter } from './types';
|
|
5
|
+
|
|
6
|
+
describe('HttpError', () => {
|
|
7
|
+
test('creates with message and statusCode', () => {
|
|
8
|
+
const err = new HttpError('Not Found', 404);
|
|
9
|
+
expect(err.message).toBe('Not Found');
|
|
10
|
+
expect(err.statusCode).toBe(404);
|
|
11
|
+
expect(err.name).toBe('HttpError');
|
|
12
|
+
expect(err.errors).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('creates with validation errors', () => {
|
|
16
|
+
const err = new HttpError('Validation Failed', 422, {
|
|
17
|
+
email: ['Email is required', 'Email must be valid'],
|
|
18
|
+
name: 'Name is required',
|
|
19
|
+
});
|
|
20
|
+
expect(err.statusCode).toBe(422);
|
|
21
|
+
expect(err.errors!.email).toEqual(['Email is required', 'Email must be valid']);
|
|
22
|
+
expect(err.errors!.name).toBe('Name is required');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('is instanceof Error', () => {
|
|
26
|
+
const err = new HttpError('Server Error', 500);
|
|
27
|
+
expect(err instanceof Error).toBe(true);
|
|
28
|
+
expect(err instanceof HttpError).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('CrudOperator types', () => {
|
|
33
|
+
test('all operators are valid', () => {
|
|
34
|
+
const operators: CrudOperator[] = [
|
|
35
|
+
'eq', 'ne', 'lt', 'gt', 'lte', 'gte',
|
|
36
|
+
'contains', 'ncontains',
|
|
37
|
+
'startswith', 'endswith',
|
|
38
|
+
'in', 'nin',
|
|
39
|
+
'null', 'nnull',
|
|
40
|
+
'between', 'nbetween',
|
|
41
|
+
];
|
|
42
|
+
expect(operators).toHaveLength(16);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Filter uses CrudOperator', () => {
|
|
46
|
+
const filter: Filter = { field: 'name', operator: 'startswith', value: 'A' };
|
|
47
|
+
expect(filter.operator).toBe('startswith');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('negation operators', () => {
|
|
51
|
+
const filters: Filter[] = [
|
|
52
|
+
{ field: 'status', operator: 'ncontains', value: 'draft' },
|
|
53
|
+
{ field: 'deleted_at', operator: 'nnull', value: null },
|
|
54
|
+
{ field: 'category', operator: 'nin', value: [1, 2] },
|
|
55
|
+
{ field: 'price', operator: 'nbetween', value: [100, 200] },
|
|
56
|
+
];
|
|
57
|
+
expect(filters).toHaveLength(4);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('LogicalFilter', () => {
|
|
62
|
+
test('or combination', () => {
|
|
63
|
+
const logical: LogicalFilter = {
|
|
64
|
+
operator: 'or',
|
|
65
|
+
value: [
|
|
66
|
+
{ field: 'status', operator: 'eq', value: 'published' },
|
|
67
|
+
{ field: 'status', operator: 'eq', value: 'draft' },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
expect(logical.operator).toBe('or');
|
|
71
|
+
expect(logical.value).toHaveLength(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('and combination', () => {
|
|
75
|
+
const logical: LogicalFilter = {
|
|
76
|
+
operator: 'and',
|
|
77
|
+
value: [
|
|
78
|
+
{ field: 'price', operator: 'gte', value: 10 },
|
|
79
|
+
{ field: 'price', operator: 'lte', value: 100 },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
expect(logical.operator).toBe('and');
|
|
83
|
+
expect(logical.value).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -47,6 +47,9 @@ const locales: Record<string, Locale> = {
|
|
|
47
47
|
'common.retry': '重试',
|
|
48
48
|
'common.yes': '是',
|
|
49
49
|
'common.no': '否',
|
|
50
|
+
'common.welcome': '欢迎使用 {title}',
|
|
51
|
+
'common.dashboardHint': '从侧边栏选择一个资源开始。',
|
|
52
|
+
'common.redirecting': '正在跳转到登录页...',
|
|
50
53
|
// Field
|
|
51
54
|
'field.enterValue': '输入{label}',
|
|
52
55
|
'field.selectPlaceholder': '请选择',
|
|
@@ -134,6 +137,9 @@ const locales: Record<string, Locale> = {
|
|
|
134
137
|
'common.retry': 'Retry',
|
|
135
138
|
'common.yes': 'Yes',
|
|
136
139
|
'common.no': 'No',
|
|
140
|
+
'common.welcome': 'Welcome to {title}',
|
|
141
|
+
'common.dashboardHint': 'Select a resource from the sidebar to get started.',
|
|
142
|
+
'common.redirecting': 'Redirecting to login...',
|
|
137
143
|
// Field
|
|
138
144
|
'field.enterValue': 'Enter {label}',
|
|
139
145
|
'field.selectPlaceholder': 'Select...',
|
|
@@ -221,3 +227,15 @@ export function t(key: string, params?: Record<string, string | number>): string
|
|
|
221
227
|
export function addTranslations(locale: string, translations: Record<string, string>): void {
|
|
222
228
|
locales[locale] = { ...locales[locale], ...translations };
|
|
223
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* useTranslation — standard i18n hook
|
|
233
|
+
* Returns { translate, getLocale, changeLocale }
|
|
234
|
+
*/
|
|
235
|
+
export function useTranslation() {
|
|
236
|
+
return {
|
|
237
|
+
translate: t,
|
|
238
|
+
getLocale,
|
|
239
|
+
changeLocale: setLocale,
|
|
240
|
+
};
|
|
241
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,36 +1,52 @@
|
|
|
1
1
|
// Core barrel exports
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
|
-
setDataProvider, getDataProvider,
|
|
4
|
+
setDataProvider, getDataProvider, getDataProviderForResource, getDataProviderNames,
|
|
5
5
|
setAuthProvider, getAuthProvider,
|
|
6
6
|
setResources, getResources, getResource,
|
|
7
7
|
setRouterProvider, getRouterProvider,
|
|
8
8
|
} from './context';
|
|
9
|
+
export type { DataProviderInput } from './context';
|
|
9
10
|
export {
|
|
10
11
|
useList, useInfiniteList,
|
|
11
12
|
useOne, useShow,
|
|
12
13
|
useSelect, useMany,
|
|
13
14
|
useCustom, useApiUrl,
|
|
15
|
+
useCustomMutation, useInvalidate,
|
|
14
16
|
useCreate, useCreateMany,
|
|
15
17
|
useUpdate, useUpdateMany,
|
|
16
18
|
useDelete, useDeleteMany,
|
|
17
19
|
useForm, useTable,
|
|
18
|
-
useNavigation,
|
|
20
|
+
useNavigation, useGo, useBack,
|
|
21
|
+
useGetToPath, useLink,
|
|
19
22
|
useResource,
|
|
23
|
+
useLog, useLogList,
|
|
24
|
+
useModalForm, useDrawerForm, useModal,
|
|
25
|
+
useOvertime, useRelation,
|
|
26
|
+
useNotification, useDataProvider,
|
|
27
|
+
useMenu, useBreadcrumb, useThemedLayoutContext,
|
|
20
28
|
} from './hooks.svelte';
|
|
21
|
-
export { matchRoute, navigate, currentPath } from './router';
|
|
29
|
+
export { matchRoute, navigate, currentPath, setActiveRouterProvider } from './router';
|
|
22
30
|
export { readURLState, writeURLState } from './url-sync';
|
|
23
31
|
export { setAccessControl, canAccess, canAccessAsync } from './permissions';
|
|
24
|
-
export { useLive } from './live';
|
|
32
|
+
export { useLive, useSubscription, usePublish } from './live';
|
|
25
33
|
export { toast } from './toast.svelte';
|
|
26
34
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
27
|
-
export { t, setLocale, getLocale, getAvailableLocales, addTranslations } from './i18n.svelte';
|
|
28
|
-
export { audit, setAuditHandler } from './audit';
|
|
35
|
+
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation } from './i18n.svelte';
|
|
36
|
+
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
37
|
+
export type { AuditLogProvider } from './audit';
|
|
29
38
|
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, colorThemes } from './theme.svelte';
|
|
30
39
|
export type { ThemeMode, ColorTheme } from './theme.svelte';
|
|
40
|
+
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier } from './unsaved-changes.svelte';
|
|
41
|
+
export { setAdminOptions, getAdminOptions, getTextTransformers } from './options';
|
|
42
|
+
export type { AdminOptions, TextTransformers, OvertimeConfig } from './options';
|
|
31
43
|
|
|
44
|
+
export * from './query-keys';
|
|
45
|
+
export { HttpError } from './types';
|
|
32
46
|
export type {
|
|
33
47
|
DataProvider, AuthProvider, NotificationProvider, MutationMode,
|
|
48
|
+
ValidationErrors, CrudOperator, LogicalFilter,
|
|
49
|
+
CustomParams, CustomResult,
|
|
34
50
|
GetListParams, GetListResult,
|
|
35
51
|
GetOneParams, GetOneResult,
|
|
36
52
|
GetManyParams, GetManyResult,
|
|
@@ -40,16 +56,19 @@ export type {
|
|
|
40
56
|
UpdateManyParams, UpdateManyResult,
|
|
41
57
|
DeleteParams, DeleteResult,
|
|
42
58
|
DeleteManyParams, DeleteManyResult,
|
|
43
|
-
CustomParams, CustomResult,
|
|
44
59
|
Pagination, Sort, Filter, Identity,
|
|
45
60
|
ResourceDefinition, FieldDefinition,
|
|
46
61
|
AuthActionResult, CheckResult,
|
|
62
|
+
ResourceTypeMap, KnownResources, InferData,
|
|
63
|
+
BaseRecord,
|
|
47
64
|
} from './types';
|
|
48
|
-
export type {
|
|
65
|
+
export type { InvalidateScope } from './options';
|
|
66
|
+
export type { LiveProvider, LiveEvent, LiveMode } from './live';
|
|
49
67
|
export type { Action, AccessControlResult, AccessControlFn } from './permissions';
|
|
50
68
|
export type { AuditEntry, AuditHandler } from './audit';
|
|
51
69
|
export { useCan } from './useCan';
|
|
52
70
|
export { useExport, useImport } from './data-transfer';
|
|
71
|
+
export type { UseExportOptions, UseImportOptions } from './data-transfer';
|
|
53
72
|
export {
|
|
54
73
|
useLogin, useLogout,
|
|
55
74
|
useRegister, useForgotPassword, useUpdatePassword,
|
|
@@ -57,7 +76,19 @@ export {
|
|
|
57
76
|
useOnError, usePermissions,
|
|
58
77
|
} from './auth-hooks.svelte';
|
|
59
78
|
export { useParsed } from './useParsed';
|
|
79
|
+
export * from './useStepsForm.svelte';
|
|
60
80
|
export { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
|
|
61
81
|
export type { RouterProvider } from './router-provider';
|
|
62
82
|
export { inferFieldType, inferResource } from './inferencer';
|
|
63
83
|
export type { InferResult } from './inferencer';
|
|
84
|
+
export { createWebSocketLiveProvider } from './live-websocket';
|
|
85
|
+
export type { WebSocketLiveProviderOptions } from './live-websocket';
|
|
86
|
+
export { createSSELiveProvider } from './live-sse';
|
|
87
|
+
export type { SSELiveProviderOptions } from './live-sse';
|
|
88
|
+
export { inferFromOpenAPI } from './inferencer-openapi';
|
|
89
|
+
export type { InferFromOpenAPIOptions } from './inferencer-openapi';
|
|
90
|
+
export {
|
|
91
|
+
getDefaultFilter, getDefaultSortOrder,
|
|
92
|
+
unionFilters, unionSorters,
|
|
93
|
+
file2Base64, generateDefaultDocumentTitle,
|
|
94
|
+
} from './helpers';
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OpenAPI → ResourceDefinition inference
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect } from 'bun:test';
|
|
5
|
+
import { inferFromOpenAPI } from './inferencer-openapi';
|
|
6
|
+
|
|
7
|
+
const sampleOpenAPI = {
|
|
8
|
+
openapi: '3.0.0',
|
|
9
|
+
info: { title: 'Blog API', version: '1.0.0' },
|
|
10
|
+
paths: {
|
|
11
|
+
'/api/posts': {
|
|
12
|
+
get: { operationId: 'getPosts' },
|
|
13
|
+
post: { operationId: 'createPost' },
|
|
14
|
+
},
|
|
15
|
+
'/api/posts/{id}': {
|
|
16
|
+
get: { operationId: 'getPost' },
|
|
17
|
+
put: { operationId: 'updatePost' },
|
|
18
|
+
delete: { operationId: 'deletePost' },
|
|
19
|
+
},
|
|
20
|
+
'/api/users': {
|
|
21
|
+
get: { operationId: 'getUsers' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
components: {
|
|
25
|
+
schemas: {
|
|
26
|
+
Post: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
required: ['title', 'content'],
|
|
29
|
+
properties: {
|
|
30
|
+
id: { type: 'integer' },
|
|
31
|
+
title: { type: 'string' },
|
|
32
|
+
content: { type: 'string', description: 'Post body content' },
|
|
33
|
+
status: { type: 'string', enum: ['draft', 'published', 'archived'] },
|
|
34
|
+
author_id: { type: 'integer' },
|
|
35
|
+
email: { type: 'string', format: 'email' },
|
|
36
|
+
created_at: { type: 'string', format: 'date-time' },
|
|
37
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
38
|
+
is_featured: { type: 'boolean' },
|
|
39
|
+
views: { type: 'integer' },
|
|
40
|
+
website: { type: 'string', format: 'uri' },
|
|
41
|
+
meta: { type: 'object' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
User: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
id: { type: 'integer' },
|
|
48
|
+
name: { type: 'string' },
|
|
49
|
+
avatar: { type: 'string', format: 'uri' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe('inferFromOpenAPI', () => {
|
|
57
|
+
test('parses both schemas', () => {
|
|
58
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
59
|
+
expect(resources.length).toBe(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('resource name is pluralized and lowercase', () => {
|
|
63
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
64
|
+
const post = resources.find(r => r.name === 'posts');
|
|
65
|
+
expect(post).toBeDefined();
|
|
66
|
+
expect(post!.label).toBe('Post');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('detects required fields', () => {
|
|
70
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
71
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
72
|
+
const titleField = post.fields.find(f => f.key === 'title')!;
|
|
73
|
+
const viewsField = post.fields.find(f => f.key === 'views')!;
|
|
74
|
+
expect(titleField.required).toBe(true);
|
|
75
|
+
expect(viewsField.required).toBeFalsy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('maps integer to number type', () => {
|
|
79
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
80
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
81
|
+
const viewsField = post.fields.find(f => f.key === 'views')!;
|
|
82
|
+
expect(viewsField.type).toBe('number');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('maps boolean type', () => {
|
|
86
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
87
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
88
|
+
const field = post.fields.find(f => f.key === 'is_featured')!;
|
|
89
|
+
expect(field.type).toBe('boolean');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('maps date-time format to date', () => {
|
|
93
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
94
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
95
|
+
const field = post.fields.find(f => f.key === 'created_at')!;
|
|
96
|
+
expect(field.type).toBe('date');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('maps email format', () => {
|
|
100
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
101
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
102
|
+
const field = post.fields.find(f => f.key === 'email')!;
|
|
103
|
+
expect(field.type).toBe('email');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('maps uri format to url', () => {
|
|
107
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
108
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
109
|
+
const field = post.fields.find(f => f.key === 'website')!;
|
|
110
|
+
expect(field.type).toBe('url');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('maps enum to select with options', () => {
|
|
114
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
115
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
116
|
+
const field = post.fields.find(f => f.key === 'status')!;
|
|
117
|
+
expect(field.type).toBe('select');
|
|
118
|
+
expect(field.options!.length).toBe(3);
|
|
119
|
+
expect(field.options![0].value).toBe('draft');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('maps array of strings to tags', () => {
|
|
123
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
124
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
125
|
+
const field = post.fields.find(f => f.key === 'tags')!;
|
|
126
|
+
expect(field.type).toBe('tags');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('maps object to json', () => {
|
|
130
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
131
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
132
|
+
const field = post.fields.find(f => f.key === 'meta')!;
|
|
133
|
+
expect(field.type).toBe('json');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('detects relation from _id suffix', () => {
|
|
137
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
138
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
139
|
+
const field = post.fields.find(f => f.key === 'author_id')!;
|
|
140
|
+
expect(field.type).toBe('relation');
|
|
141
|
+
expect(field.resource).toBe('authors');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('detects CRUD capabilities from paths', () => {
|
|
145
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
146
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
147
|
+
expect(post.canCreate).toBe(true);
|
|
148
|
+
expect(post.canEdit).toBe(true);
|
|
149
|
+
expect(post.canDelete).toBe(true);
|
|
150
|
+
expect(post.canShow).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('read-only resource has limited capabilities', () => {
|
|
154
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
155
|
+
const user = resources.find(r => r.name === 'users')!;
|
|
156
|
+
expect(user.canShow).toBe(true);
|
|
157
|
+
expect(user.canCreate).toBe(false);
|
|
158
|
+
expect(user.canDelete).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('include filter works', () => {
|
|
162
|
+
const resources = inferFromOpenAPI(sampleOpenAPI, { include: ['Post'] });
|
|
163
|
+
expect(resources.length).toBe(1);
|
|
164
|
+
expect(resources[0].name).toBe('posts');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('exclude filter works', () => {
|
|
168
|
+
const resources = inferFromOpenAPI(sampleOpenAPI, { exclude: ['User'] });
|
|
169
|
+
expect(resources.length).toBe(1);
|
|
170
|
+
expect(resources[0].name).toBe('posts');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('content field is detected as textarea from key name hint', () => {
|
|
174
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
175
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
176
|
+
const field = post.fields.find(f => f.key === 'content')!;
|
|
177
|
+
expect(field.type).toBe('textarea');
|
|
178
|
+
expect(field.showInList).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('id field excluded from form', () => {
|
|
182
|
+
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
183
|
+
const post = resources.find(r => r.name === 'posts')!;
|
|
184
|
+
const idField = post.fields.find(f => f.key === 'id')!;
|
|
185
|
+
expect(idField.showInForm).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('empty spec returns empty array', () => {
|
|
189
|
+
const resources = inferFromOpenAPI({});
|
|
190
|
+
expect(resources.length).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
});
|