@svadmin/core 0.25.1 → 0.25.3

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.
@@ -1,177 +0,0 @@
1
- // Unit tests for helpers-pure.ts — pure functions, no Svelte runtime
2
- import { describe, test, expect } from 'bun:test';
3
- import {
4
- getDefaultFilter, getDefaultSortOrder,
5
- unionFilters, unionSorters,
6
- parseCSV,
7
- } from './helpers-pure';
8
- import type { Filter, Sort } from './types';
9
-
10
- // ─── getDefaultFilter ─────────────────────────────────────────
11
-
12
- describe('getDefaultFilter', () => {
13
- const filters: Filter[] = [
14
- { field: 'status', operator: 'eq', value: 'published' },
15
- { field: 'price', operator: 'gte', value: 100 },
16
- { field: 'title', operator: 'contains', value: 'hello' },
17
- ];
18
-
19
- test('returns value for matching field+operator', () => {
20
- expect(getDefaultFilter('status', filters)).toBe('published');
21
- expect(getDefaultFilter('price', filters, 'gte')).toBe(100);
22
- });
23
-
24
- test('returns undefined for non-matching field', () => {
25
- expect(getDefaultFilter('nonexistent', filters)).toBeUndefined();
26
- });
27
-
28
- test('returns undefined for wrong operator', () => {
29
- expect(getDefaultFilter('status', filters, 'contains')).toBeUndefined();
30
- });
31
-
32
- test('returns undefined for empty/undefined filters', () => {
33
- expect(getDefaultFilter('status', [])).toBeUndefined();
34
- expect(getDefaultFilter('status', undefined)).toBeUndefined();
35
- });
36
- });
37
-
38
- // ─── getDefaultSortOrder ──────────────────────────────────────
39
-
40
- describe('getDefaultSortOrder', () => {
41
- const sorters: Sort[] = [
42
- { field: 'name', order: 'asc' },
43
- { field: 'createdAt', order: 'desc' },
44
- ];
45
-
46
- test('returns order for matching field', () => {
47
- expect(getDefaultSortOrder('name', sorters)).toBe('asc');
48
- expect(getDefaultSortOrder('createdAt', sorters)).toBe('desc');
49
- });
50
-
51
- test('returns undefined for non-matching field', () => {
52
- expect(getDefaultSortOrder('unknown', sorters)).toBeUndefined();
53
- });
54
-
55
- test('returns undefined for empty/undefined sorters', () => {
56
- expect(getDefaultSortOrder('name', undefined)).toBeUndefined();
57
- expect(getDefaultSortOrder('name', [])).toBeUndefined();
58
- });
59
- });
60
-
61
- // ─── unionFilters ─────────────────────────────────────────────
62
-
63
- describe('unionFilters', () => {
64
- test('merges new filters over previous', () => {
65
- const prev: Filter[] = [
66
- { field: 'status', operator: 'eq', value: 'draft' },
67
- ];
68
- const next: Filter[] = [
69
- { field: 'status', operator: 'eq', value: 'published' },
70
- ];
71
- const result = unionFilters([], next, prev);
72
- expect(result).toHaveLength(1);
73
- expect(result[0].value).toBe('published');
74
- });
75
-
76
- test('permanent filters override everything', () => {
77
- const permanent: Filter[] = [
78
- { field: 'org', operator: 'eq', value: 'acme' },
79
- ];
80
- const next: Filter[] = [
81
- { field: 'org', operator: 'eq', value: 'other' },
82
- ];
83
- const result = unionFilters(permanent, next, []);
84
- expect(result).toHaveLength(1);
85
- expect(result[0].value).toBe('acme');
86
- });
87
-
88
- test('removes filters with undefined value', () => {
89
- const prev: Filter[] = [
90
- { field: 'status', operator: 'eq', value: 'draft' },
91
- ];
92
- const next: Filter[] = [
93
- { field: 'status', operator: 'eq', value: undefined },
94
- ];
95
- const result = unionFilters([], next, prev);
96
- expect(result).toHaveLength(0);
97
- });
98
-
99
- test('adds new filters not in previous', () => {
100
- const prev: Filter[] = [
101
- { field: 'status', operator: 'eq', value: 'draft' },
102
- ];
103
- const next: Filter[] = [
104
- { field: 'category', operator: 'eq', value: 'tech' },
105
- ];
106
- const result = unionFilters([], next, prev);
107
- expect(result).toHaveLength(2);
108
- });
109
-
110
- test('handles empty arrays', () => {
111
- expect(unionFilters([], [], [])).toEqual([]);
112
- });
113
- });
114
-
115
- // ─── unionSorters ─────────────────────────────────────────────
116
-
117
- describe('unionSorters', () => {
118
- test('new sorters take priority', () => {
119
- const permanent: Sort[] = [{ field: 'id', order: 'asc' }];
120
- const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
121
- const result = unionSorters(permanent, newSorters);
122
- expect(result).toHaveLength(2);
123
- expect(result[0].field).toBe('name');
124
- });
125
-
126
- test('permanent sorters override new sorters on same field', () => {
127
- const permanent: Sort[] = [{ field: 'name', order: 'asc' }];
128
- const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
129
- const result = unionSorters(permanent, newSorters);
130
- expect(result).toHaveLength(1);
131
- expect(result[0].order).toBe('asc');
132
- });
133
-
134
- test('empty arrays', () => {
135
- expect(unionSorters([], [])).toEqual([]);
136
- });
137
- });
138
-
139
- // ─── parseCSV ─────────────────────────────────────────────────
140
-
141
- describe('parseCSV', () => {
142
- test('parses simple CSV', () => {
143
- const rows = parseCSV('name,age\nAlice,30\nBob,25');
144
- expect(rows).toHaveLength(3);
145
- expect(rows[0]).toEqual(['name', 'age']);
146
- expect(rows[1]).toEqual(['Alice', '30']);
147
- expect(rows[2]).toEqual(['Bob', '25']);
148
- });
149
-
150
- test('handles quoted fields', () => {
151
- const rows = parseCSV('name,bio\nAlice,"Hello, World"');
152
- expect(rows).toHaveLength(2);
153
- expect(rows[1][1]).toBe('Hello, World');
154
- });
155
-
156
- test('handles escaped quotes', () => {
157
- const rows = parseCSV('val\n"He said ""hello"""');
158
- expect(rows).toHaveLength(2);
159
- expect(rows[1][0]).toBe('He said "hello"');
160
- });
161
-
162
- test('handles CRLF line endings', () => {
163
- const rows = parseCSV('a,b\r\n1,2\r\n3,4');
164
- expect(rows).toHaveLength(3);
165
- });
166
-
167
- test('skips empty rows', () => {
168
- const rows = parseCSV('a\n\nb\n');
169
- expect(rows).toHaveLength(2);
170
- expect(rows[0]).toEqual(['a']);
171
- expect(rows[1]).toEqual(['b']);
172
- });
173
-
174
- test('handles empty input', () => {
175
- expect(parseCSV('')).toEqual([]);
176
- });
177
- });
@@ -1,85 +0,0 @@
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.test.ts DELETED
@@ -1,90 +0,0 @@
1
- // Unit tests for URL parsing (useParsed logic)
2
- // We test the pure parsing logic directly since the hook itself uses Svelte runes.
3
- import { describe, test, expect } from 'bun:test';
4
-
5
- /**
6
- * Parse a hash-based URL into structured route info.
7
- * This mirrors the logic in useParsed.ts.
8
- */
9
- function parseHash(hash: string) {
10
- const raw = hash.startsWith('#') ? hash.slice(1) : hash;
11
- const [pathPart, queryPart] = raw.split('?');
12
- const segments = pathPart.split('/').filter(Boolean);
13
-
14
- const params: Record<string, string> = {};
15
- if (queryPart) {
16
- for (const pair of queryPart.split('&')) {
17
- const [k, v] = pair.split('=');
18
- if (k) params[decodeURIComponent(k)] = decodeURIComponent(v ?? '');
19
- }
20
- }
21
-
22
- return {
23
- resource: segments[0],
24
- action: segments[1],
25
- id: segments[2],
26
- params,
27
- };
28
- }
29
-
30
- describe('parseHash (useParsed logic)', () => {
31
- test('empty hash', () => {
32
- const result = parseHash('');
33
- expect(result.resource).toBeUndefined();
34
- expect(result.action).toBeUndefined();
35
- expect(result.id).toBeUndefined();
36
- expect(result.params).toEqual({});
37
- });
38
-
39
- test('root hash', () => {
40
- const result = parseHash('#/');
41
- expect(result.resource).toBeUndefined();
42
- });
43
-
44
- test('resource only', () => {
45
- const result = parseHash('#/posts');
46
- expect(result.resource).toBe('posts');
47
- expect(result.action).toBeUndefined();
48
- expect(result.id).toBeUndefined();
49
- });
50
-
51
- test('resource and action', () => {
52
- const result = parseHash('#/posts/create');
53
- expect(result.resource).toBe('posts');
54
- expect(result.action).toBe('create');
55
- });
56
-
57
- test('resource, action, and id', () => {
58
- const result = parseHash('#/posts/edit/42');
59
- expect(result.resource).toBe('posts');
60
- expect(result.action).toBe('edit');
61
- expect(result.id).toBe('42');
62
- });
63
-
64
- test('with query params', () => {
65
- const result = parseHash('#/posts?page=2&sort=name');
66
- expect(result.resource).toBe('posts');
67
- expect(result.params.page).toBe('2');
68
- expect(result.params.sort).toBe('name');
69
- });
70
-
71
- test('resource with action and query', () => {
72
- const result = parseHash('#/posts/edit/5?tab=details');
73
- expect(result.resource).toBe('posts');
74
- expect(result.action).toBe('edit');
75
- expect(result.id).toBe('5');
76
- expect(result.params.tab).toBe('details');
77
- });
78
-
79
- test('encoded query params', () => {
80
- const result = parseHash('#/search?q=hello%20world&filter=tag%3Djs');
81
- expect(result.params.q).toBe('hello world');
82
- expect(result.params.filter).toBe('tag=js');
83
- });
84
-
85
- test('login route', () => {
86
- const result = parseHash('#/login');
87
- expect(result.resource).toBe('login');
88
- expect(result.action).toBeUndefined();
89
- });
90
- });
@@ -1,192 +0,0 @@
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
- });