@userfrosting/sprinkle-core 6.0.0-alpha.5 → 6.0.0-beta.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/app/assets/composables/index.ts +3 -1
- package/app/assets/composables/useAxiosInterceptor.ts +30 -0
- package/app/assets/composables/useCsrf.ts +10 -1
- package/app/assets/composables/useRuleSchemaAdapter.ts +219 -0
- package/app/assets/composables/{sprunjer.ts → useSprunjer.ts} +10 -3
- package/app/assets/index.ts +8 -0
- package/app/assets/interfaces/ApiResponse.ts +8 -1
- package/app/assets/interfaces/index.ts +1 -1
- package/app/assets/interfaces/sprunjer.ts +2 -1
- package/app/assets/routes/index.ts +4 -4
- package/app/assets/stores/index.ts +2 -1
- package/app/assets/stores/useAlertsStore.ts +30 -0
- package/app/assets/tests/composables/useCsrf.test.ts +2 -2
- package/app/assets/tests/composables/useRuleSchemaAdapter.test.ts +564 -0
- package/app/assets/tests/plugin.test.ts +2 -2
- package/app/assets/tests/stores/config.test.ts +1 -1
- package/package.json +6 -3
- /package/app/assets/stores/{config.ts → useConfigStore.ts} +0 -0
- /package/app/assets/views/{401Unauthorized.vue → Page401Unauthorized.vue} +0 -0
- /package/app/assets/views/{403Forbidden.vue → Page403Forbidden.vue} +0 -0
- /package/app/assets/views/{404NotFound.vue → Page404NotFound.vue} +0 -0
- /package/app/assets/views/{ErrorPage.vue → PageError.vue} +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { useAlertsStore } from '../stores/useAlertsStore'
|
|
3
|
+
import { Severity } from '../interfaces'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Axios Error Handler
|
|
7
|
+
*
|
|
8
|
+
* This composable sets up an Axios interceptor to handle errors globally using
|
|
9
|
+
* the Alerts store. It sends the error to the Alerts store, unless it's a 401
|
|
10
|
+
* error.
|
|
11
|
+
*
|
|
12
|
+
* @see https://axios-http.com/docs/interceptors
|
|
13
|
+
*/
|
|
14
|
+
export const useAxiosInterceptor = () => {
|
|
15
|
+
axios.interceptors.response.use(
|
|
16
|
+
(response) => response,
|
|
17
|
+
(error) => {
|
|
18
|
+
if (error.response.status !== 401) {
|
|
19
|
+
const alertStore = useAlertsStore()
|
|
20
|
+
alertStore.push({
|
|
21
|
+
title: error.response.data.title ?? error.response?.statusText,
|
|
22
|
+
description: error.response?.data?.description ?? error.message,
|
|
23
|
+
style: Severity.Danger
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Promise.reject(error)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -34,6 +34,14 @@ export const useCsrf = () => {
|
|
|
34
34
|
axios.defaults.headers.patch[key_value.value] = token.value
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the CSRF token from the server
|
|
39
|
+
*/
|
|
40
|
+
async function fetchCsrfToken() {
|
|
41
|
+
const response = await axios.get('/api/csrf')
|
|
42
|
+
updateFromHeaders(response.headers)
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
/**
|
|
38
46
|
* Get the CSRF token name and value keys from config.
|
|
39
47
|
*/
|
|
@@ -115,6 +123,7 @@ export const useCsrf = () => {
|
|
|
115
123
|
name,
|
|
116
124
|
token,
|
|
117
125
|
isEnabled,
|
|
118
|
-
updateFromHeaders
|
|
126
|
+
updateFromHeaders,
|
|
127
|
+
fetchCsrfToken
|
|
119
128
|
}
|
|
120
129
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { parse as YamlParse } from 'yaml'
|
|
2
|
+
import {
|
|
3
|
+
withMessage,
|
|
4
|
+
required,
|
|
5
|
+
maxLength,
|
|
6
|
+
minLength,
|
|
7
|
+
email,
|
|
8
|
+
integer,
|
|
9
|
+
numeric,
|
|
10
|
+
url,
|
|
11
|
+
oneOf,
|
|
12
|
+
between,
|
|
13
|
+
regex,
|
|
14
|
+
not
|
|
15
|
+
} from '@regle/rules'
|
|
16
|
+
import { useTranslator } from '../stores'
|
|
17
|
+
|
|
18
|
+
export function useRuleSchemaAdapter() {
|
|
19
|
+
/**
|
|
20
|
+
* Parse the YAML schema string into a JavaScript object.
|
|
21
|
+
*
|
|
22
|
+
* @param rawSchema The YAML schema string to parse.
|
|
23
|
+
* @returns RuleSchema The Regle schema object.
|
|
24
|
+
*/
|
|
25
|
+
function adapt(rawSchema: string) {
|
|
26
|
+
// The YAML data parsed to a JavaScript object
|
|
27
|
+
const sourceSchema = parse(rawSchema)
|
|
28
|
+
|
|
29
|
+
// The Regle schema object to be returned
|
|
30
|
+
const regleSchema: any = {}
|
|
31
|
+
|
|
32
|
+
// Iterate over each field in the schema
|
|
33
|
+
for (const field in sourceSchema) {
|
|
34
|
+
// Check if the field is a direct property of the sourceSchema object
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(sourceSchema, field)) {
|
|
36
|
+
// Get the field rules from the source schema
|
|
37
|
+
const schemaFieldRules = sourceSchema[field]?.validators || {}
|
|
38
|
+
|
|
39
|
+
// The returned regle rules for the field
|
|
40
|
+
const regleRules: Record<string, any> = {}
|
|
41
|
+
|
|
42
|
+
// Iterate over each rule in the source schema field rules
|
|
43
|
+
for (const key of Object.keys(schemaFieldRules)) {
|
|
44
|
+
adaptRule(key, schemaFieldRules, regleRules)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
regleSchema[field] = regleRules
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return regleSchema
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse the YAML schema string into a JavaScript object.
|
|
56
|
+
*
|
|
57
|
+
* @param rawSchema The YAML schema string to parse.
|
|
58
|
+
* @returns The parsed YAML schema as a JavaScript object.
|
|
59
|
+
*/
|
|
60
|
+
function parse(rawSchema: string): Record<string, any> {
|
|
61
|
+
return YamlParse(rawSchema)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function translateMessage(fieldRulesMeta: { message?: string; [key: string]: any }): string {
|
|
65
|
+
const { translate } = useTranslator()
|
|
66
|
+
|
|
67
|
+
// If there's no message, return an empty string
|
|
68
|
+
if (!fieldRulesMeta.message) {
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Copy the field rules meta to avoid mutation and remove the message key
|
|
73
|
+
const fieldRulesMetaCopy = { ...fieldRulesMeta }
|
|
74
|
+
delete fieldRulesMetaCopy.message
|
|
75
|
+
|
|
76
|
+
return translate(fieldRulesMeta.message, fieldRulesMetaCopy)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function adaptRule(key: string, schemaFieldRules: any, regleRules: Record<string, any>) {
|
|
80
|
+
// Required
|
|
81
|
+
if (key === 'required' && schemaFieldRules.required) {
|
|
82
|
+
const message: string = translateMessage(schemaFieldRules.required)
|
|
83
|
+
regleRules['required'] = message === '' ? required : withMessage(required, message)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Email
|
|
87
|
+
if (key === 'email' && schemaFieldRules.email) {
|
|
88
|
+
const message: string = translateMessage(schemaFieldRules.email)
|
|
89
|
+
regleRules['email'] = withMessage(email, message)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Length
|
|
93
|
+
if (key === 'length' && schemaFieldRules.length) {
|
|
94
|
+
if (schemaFieldRules.length.min !== undefined) {
|
|
95
|
+
const message: string = translateMessage(schemaFieldRules.length)
|
|
96
|
+
regleRules['minLength'] =
|
|
97
|
+
message === ''
|
|
98
|
+
? minLength(schemaFieldRules.length.min)
|
|
99
|
+
: withMessage(minLength(schemaFieldRules.length.min), message)
|
|
100
|
+
}
|
|
101
|
+
if (schemaFieldRules.length.max !== undefined) {
|
|
102
|
+
const message: string = translateMessage(schemaFieldRules.length)
|
|
103
|
+
regleRules['maxLength'] =
|
|
104
|
+
message === ''
|
|
105
|
+
? maxLength(schemaFieldRules.length.max)
|
|
106
|
+
: withMessage(maxLength(schemaFieldRules.length.max), message)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// TODO : matches
|
|
111
|
+
if (key === 'matches' && schemaFieldRules.matches) {
|
|
112
|
+
console.warn('Validation rule "matches" not implemented yet')
|
|
113
|
+
// console.debug('Matched rule: matches', schemaFieldRules.matches)
|
|
114
|
+
// sameAs: sameAs(() => form.value.password),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// TODO : equals
|
|
118
|
+
if (key === 'equals' && schemaFieldRules.equals) {
|
|
119
|
+
console.warn('Validation rule "equals" not implemented yet')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Integer
|
|
123
|
+
if (key === 'integer' && schemaFieldRules.integer) {
|
|
124
|
+
const message: string = translateMessage(schemaFieldRules.integer)
|
|
125
|
+
regleRules['integer'] = message === '' ? integer : withMessage(integer, message)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// member_of
|
|
129
|
+
if (key === 'member_of' && schemaFieldRules.member_of) {
|
|
130
|
+
const message: string = translateMessage(schemaFieldRules.member_of)
|
|
131
|
+
regleRules['member_of'] =
|
|
132
|
+
message === ''
|
|
133
|
+
? oneOf(schemaFieldRules.member_of.values)
|
|
134
|
+
: withMessage(oneOf(schemaFieldRules.member_of.values), message)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// no_leading_whitespace
|
|
138
|
+
if (key === 'no_leading_whitespace' && schemaFieldRules.no_leading_whitespace) {
|
|
139
|
+
const message: string = translateMessage(schemaFieldRules.no_leading_whitespace)
|
|
140
|
+
regleRules['no_leading_whitespace'] =
|
|
141
|
+
message === '' ? regex(/^\S.*$/) : withMessage(regex(/^\S.*$/), message)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// no_trailing_whitespace
|
|
145
|
+
if (key === 'no_trailing_whitespace' && schemaFieldRules.no_trailing_whitespace) {
|
|
146
|
+
const message: string = translateMessage(schemaFieldRules.no_trailing_whitespace)
|
|
147
|
+
regleRules['no_trailing_whitespace'] =
|
|
148
|
+
message === '' ? regex(/^.*\S$/) : withMessage(regex(/^.*\S$/), message)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// TODO : not_equals
|
|
152
|
+
if (key === 'not_equals' && schemaFieldRules.not_equals) {
|
|
153
|
+
console.warn('Validation rule "not_equals" not implemented yet')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// TODO : not_matches
|
|
157
|
+
if (key === 'not_matches' && schemaFieldRules.not_matches) {
|
|
158
|
+
console.warn('Validation rule "not_matches" not implemented yet')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// not_member_of
|
|
162
|
+
if (key === 'not_member_of' && schemaFieldRules.not_member_of) {
|
|
163
|
+
const message: string = translateMessage(schemaFieldRules.not_member_of)
|
|
164
|
+
regleRules['not_member_of'] =
|
|
165
|
+
message === ''
|
|
166
|
+
? not(oneOf(schemaFieldRules.not_member_of.values))
|
|
167
|
+
: withMessage(not(oneOf(schemaFieldRules.not_member_of.values)), message)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Numeric
|
|
171
|
+
if (key === 'numeric' && schemaFieldRules.numeric) {
|
|
172
|
+
const message: string = translateMessage(schemaFieldRules.numeric)
|
|
173
|
+
regleRules['numeric'] = message === '' ? numeric : withMessage(numeric, message)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Range
|
|
177
|
+
if (key === 'range' && schemaFieldRules.range) {
|
|
178
|
+
const message: string = translateMessage(schemaFieldRules.range)
|
|
179
|
+
regleRules['range'] =
|
|
180
|
+
message === ''
|
|
181
|
+
? between(schemaFieldRules.range.min, schemaFieldRules.range.max)
|
|
182
|
+
: withMessage(
|
|
183
|
+
between(schemaFieldRules.range.min, schemaFieldRules.range.max),
|
|
184
|
+
message
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Regex
|
|
189
|
+
if (key === 'regex' && schemaFieldRules.regex) {
|
|
190
|
+
const message: string = translateMessage(schemaFieldRules.regex)
|
|
191
|
+
regleRules['regex'] =
|
|
192
|
+
message === ''
|
|
193
|
+
? regex(new RegExp(schemaFieldRules.regex.regex))
|
|
194
|
+
: withMessage(regex(new RegExp(schemaFieldRules.regex.regex)), message)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TODO : telephone
|
|
198
|
+
if (key === 'telephone' && schemaFieldRules.telephone) {
|
|
199
|
+
console.warn('Validation rule "telephone" not implemented yet')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// uri
|
|
203
|
+
if (key === 'uri' && schemaFieldRules.uri) {
|
|
204
|
+
const message: string = translateMessage(schemaFieldRules.uri)
|
|
205
|
+
regleRules['uri'] = message === '' ? url : withMessage(url, message)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Username
|
|
209
|
+
if (key === 'username' && schemaFieldRules.username) {
|
|
210
|
+
const message: string = translateMessage(schemaFieldRules.username)
|
|
211
|
+
regleRules['username'] =
|
|
212
|
+
message === ''
|
|
213
|
+
? regex(/^([a-z0-9.\-_])+$/i)
|
|
214
|
+
: withMessage(regex(/^([a-z0-9.\-_])+$/i), message)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { adapt, parse, translateMessage }
|
|
219
|
+
}
|
|
@@ -33,7 +33,13 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { ref, toValue, watchEffect, computed } from 'vue'
|
|
35
35
|
import axios from 'axios'
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
ApiErrorResponse,
|
|
38
|
+
AssociativeArray,
|
|
39
|
+
Sprunjer,
|
|
40
|
+
SprunjerData,
|
|
41
|
+
SprunjerResponse
|
|
42
|
+
} from '../interfaces'
|
|
37
43
|
|
|
38
44
|
export const useSprunjer = (
|
|
39
45
|
dataUrl: string | (() => string),
|
|
@@ -60,6 +66,7 @@ export const useSprunjer = (
|
|
|
60
66
|
|
|
61
67
|
// State
|
|
62
68
|
const loading = ref<boolean>(false)
|
|
69
|
+
const error = ref<ApiErrorResponse | null>(null)
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* Api fetch function
|
|
@@ -87,8 +94,7 @@ export const useSprunjer = (
|
|
|
87
94
|
data.value.filterable = response.data.filterable ?? []
|
|
88
95
|
})
|
|
89
96
|
.catch((err) => {
|
|
90
|
-
|
|
91
|
-
console.error(err)
|
|
97
|
+
error.value = err.response.data as ApiErrorResponse
|
|
92
98
|
})
|
|
93
99
|
.finally(() => {
|
|
94
100
|
loading.value = false
|
|
@@ -169,6 +175,7 @@ export const useSprunjer = (
|
|
|
169
175
|
data,
|
|
170
176
|
fetch,
|
|
171
177
|
loading,
|
|
178
|
+
error,
|
|
172
179
|
downloadCsv,
|
|
173
180
|
totalPages,
|
|
174
181
|
countFiltered,
|
package/app/assets/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { App } from 'vue'
|
|
2
2
|
import { useConfigStore, useTranslator } from './stores'
|
|
3
3
|
import { useCsrf } from './composables/useCsrf'
|
|
4
|
+
import { useAxiosInterceptor } from './composables'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Core Sprinkle initialization recipe.
|
|
@@ -11,6 +12,13 @@ import { useCsrf } from './composables/useCsrf'
|
|
|
11
12
|
*/
|
|
12
13
|
export default {
|
|
13
14
|
install: (app: App) => {
|
|
15
|
+
/**
|
|
16
|
+
* Add Axios error handler.
|
|
17
|
+
* Load first to ensure that all axios requests are intercepted.
|
|
18
|
+
* This is important for the config loading and translator loading.
|
|
19
|
+
*/
|
|
20
|
+
useAxiosInterceptor()
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Load configuration
|
|
16
24
|
*/
|
|
@@ -27,4 +27,4 @@ export type { SprunjerRequest, SprunjerResponse } from './sprunjerApi'
|
|
|
27
27
|
export type { DictionaryResponse, DictionaryEntries, DictionaryConfig } from './DictionaryApi'
|
|
28
28
|
|
|
29
29
|
// Misc
|
|
30
|
-
export type { ApiResponse } from './ApiResponse'
|
|
30
|
+
export type { ApiResponse, ApiErrorResponse } from './ApiResponse'
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Represents the interface for the Sprunjer composable.
|
|
5
5
|
*/
|
|
6
6
|
import type { Ref, ComputedRef } from 'vue'
|
|
7
|
-
import type { AssociativeArray } from '.'
|
|
7
|
+
import type { ApiErrorResponse, AssociativeArray } from '.'
|
|
8
8
|
|
|
9
9
|
export interface Sprunjer {
|
|
10
10
|
dataUrl: string | (() => string)
|
|
@@ -15,6 +15,7 @@ export interface Sprunjer {
|
|
|
15
15
|
data: Ref<SprunjerData>
|
|
16
16
|
fetch: () => void
|
|
17
17
|
loading: Ref<boolean>
|
|
18
|
+
error: Ref<ApiErrorResponse | null>
|
|
18
19
|
totalPages: ComputedRef<number>
|
|
19
20
|
downloadCsv: () => void
|
|
20
21
|
countFiltered: ComputedRef<number>
|
|
@@ -12,7 +12,7 @@ export default [
|
|
|
12
12
|
title: 'ERROR.404.TITLE',
|
|
13
13
|
description: 'ERROR.404.DESCRIPTION'
|
|
14
14
|
},
|
|
15
|
-
component: () => import('../views/
|
|
15
|
+
component: () => import('../views/Page404NotFound.vue')
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
path: '/:pathMatch(.*)*',
|
|
@@ -21,7 +21,7 @@ export default [
|
|
|
21
21
|
title: 'ERROR.401.TITLE',
|
|
22
22
|
description: 'ERROR.401.DESCRIPTION'
|
|
23
23
|
},
|
|
24
|
-
component: () => import('../views/
|
|
24
|
+
component: () => import('../views/Page401Unauthorized.vue')
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
path: '/:pathMatch(.*)*',
|
|
@@ -30,7 +30,7 @@ export default [
|
|
|
30
30
|
title: 'ERROR.403.TITLE',
|
|
31
31
|
description: 'ERROR.403.DESCRIPTION'
|
|
32
32
|
},
|
|
33
|
-
component: () => import('../views/
|
|
33
|
+
component: () => import('../views/Page403Forbidden.vue')
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
path: '/:pathMatch(.*)*',
|
|
@@ -39,6 +39,6 @@ export default [
|
|
|
39
39
|
title: 'ERROR.TITLE',
|
|
40
40
|
description: 'ERROR.DESCRIPTION'
|
|
41
41
|
},
|
|
42
|
-
component: () => import('../views/
|
|
42
|
+
component: () => import('../views/PageError.vue')
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import type { AlertInterface } from '../interfaces'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Alerts Store
|
|
6
|
+
*
|
|
7
|
+
* Manages application alerts in a reactive store. Templates can use this
|
|
8
|
+
* store to display alerts to users, including errors, warnings,
|
|
9
|
+
* informational, or success messages. When an alert is added, templates
|
|
10
|
+
* should automatically update the interface to reflect the change.
|
|
11
|
+
*/
|
|
12
|
+
export const useAlertsStore = defineStore('alerts', {
|
|
13
|
+
state: () => ({
|
|
14
|
+
alerts: [] as AlertInterface[]
|
|
15
|
+
}),
|
|
16
|
+
actions: {
|
|
17
|
+
push(alert: AlertInterface) {
|
|
18
|
+
this.alerts.push(alert)
|
|
19
|
+
},
|
|
20
|
+
pop() {
|
|
21
|
+
return this.alerts.pop()
|
|
22
|
+
},
|
|
23
|
+
shift() {
|
|
24
|
+
return this.alerts.shift()
|
|
25
|
+
},
|
|
26
|
+
clear() {
|
|
27
|
+
this.alerts = []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, beforeEach, afterEach, test, vi } from 'vitest'
|
|
2
2
|
import axios from 'axios'
|
|
3
|
-
import { useConfigStore } from '../../stores/
|
|
3
|
+
import { useConfigStore } from '../../stores/useConfigStore'
|
|
4
4
|
import { useCsrf } from '../../composables/useCsrf'
|
|
5
5
|
import { nextTick } from 'vue'
|
|
6
6
|
|
|
7
7
|
// Mock the config store
|
|
8
|
-
vi.mock('../../stores/
|
|
8
|
+
vi.mock('../../stores/useConfigStore')
|
|
9
9
|
const mockUseConfigStore = {
|
|
10
10
|
get: vi.fn()
|
|
11
11
|
}
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { describe, test, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { useRuleSchemaAdapter } from '../../composables/useRuleSchemaAdapter'
|
|
3
|
+
import { useRegle } from '@regle/core'
|
|
4
|
+
|
|
5
|
+
// Mock the translator store
|
|
6
|
+
const translateMock = vi.fn((key: string) => key || '')
|
|
7
|
+
vi.mock('@userfrosting/sprinkle-core/stores', () => ({
|
|
8
|
+
useTranslator: () => ({
|
|
9
|
+
translate: translateMock
|
|
10
|
+
})
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
describe('useRuleSchemaAdapter', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.clearAllMocks()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('should parse a basic schema', () => {
|
|
19
|
+
const yamlInput = `
|
|
20
|
+
foo:
|
|
21
|
+
validators:
|
|
22
|
+
length:
|
|
23
|
+
min: 1
|
|
24
|
+
max: 132
|
|
25
|
+
required: true
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const { r$ } = useRegle(
|
|
29
|
+
{
|
|
30
|
+
foo: ''
|
|
31
|
+
},
|
|
32
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(r$.foo).toBeDefined()
|
|
36
|
+
expect(r$.foo.$rules.required).toBeDefined()
|
|
37
|
+
expect(r$.foo.$rules.minLength).toBeDefined()
|
|
38
|
+
expect(r$.foo.$rules.maxLength).toBeDefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should parse a schema with a custom message', () => {
|
|
42
|
+
const yamlInput = `
|
|
43
|
+
name:
|
|
44
|
+
validators:
|
|
45
|
+
length:
|
|
46
|
+
min: 2
|
|
47
|
+
max: 20
|
|
48
|
+
required: true
|
|
49
|
+
email:
|
|
50
|
+
validators:
|
|
51
|
+
length:
|
|
52
|
+
min: 1
|
|
53
|
+
max: 30
|
|
54
|
+
email: true
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
const { r$ } = useRegle(
|
|
58
|
+
{
|
|
59
|
+
name: '',
|
|
60
|
+
email: ''
|
|
61
|
+
},
|
|
62
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(r$.name).toBeDefined()
|
|
66
|
+
expect(r$.name.$rules.required).toBeDefined()
|
|
67
|
+
expect(r$.name.$rules.minLength).toBeDefined()
|
|
68
|
+
expect(r$.name.$rules.maxLength).toBeDefined()
|
|
69
|
+
expect(r$.email).toBeDefined()
|
|
70
|
+
expect(r$.email.$rules.required).not.toBeDefined()
|
|
71
|
+
expect(r$.email.$rules.minLength).toBeDefined()
|
|
72
|
+
expect(r$.email.$rules.maxLength).toBeDefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should parse a bad schema without errors', () => {
|
|
76
|
+
// N.B.: The schema is missing the 'validators' key
|
|
77
|
+
const yamlInput = `
|
|
78
|
+
foo:
|
|
79
|
+
length:
|
|
80
|
+
min: 1
|
|
81
|
+
max: 132
|
|
82
|
+
required: true
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
const { r$ } = useRegle(
|
|
86
|
+
{
|
|
87
|
+
foo: ''
|
|
88
|
+
},
|
|
89
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
expect(r$.foo).toBeDefined()
|
|
93
|
+
expect(r$.foo.$rules.required).not.toBeDefined()
|
|
94
|
+
expect(r$.foo.$rules.minLength).not.toBeDefined()
|
|
95
|
+
expect(r$.foo.$rules.maxLength).not.toBeDefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('required rule', () => {
|
|
99
|
+
const yamlInput = `
|
|
100
|
+
first_name:
|
|
101
|
+
validators:
|
|
102
|
+
required:
|
|
103
|
+
label: "&FIRST_NAME"
|
|
104
|
+
message: VALIDATE.REQUIRED
|
|
105
|
+
last_name:
|
|
106
|
+
validators:
|
|
107
|
+
required: true
|
|
108
|
+
`
|
|
109
|
+
|
|
110
|
+
const { r$ } = useRegle(
|
|
111
|
+
{
|
|
112
|
+
first_name: '',
|
|
113
|
+
last_name: ''
|
|
114
|
+
},
|
|
115
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Set translator expectations
|
|
119
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.REQUIRED', {
|
|
120
|
+
label: '&FIRST_NAME'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(r$.first_name.$rules.required.$message).toBe('VALIDATE.REQUIRED') // Custom message
|
|
124
|
+
expect(r$.last_name.$rules.required.$message).toBe('This field is required') // Default message
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('email rule', () => {
|
|
128
|
+
const yamlInput = `
|
|
129
|
+
email:
|
|
130
|
+
validators:
|
|
131
|
+
email:
|
|
132
|
+
message: VALIDATE.INVALID_EMAIL
|
|
133
|
+
email2:
|
|
134
|
+
validators:
|
|
135
|
+
email: true
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
const { r$ } = useRegle(
|
|
139
|
+
{
|
|
140
|
+
email: '',
|
|
141
|
+
email2: ''
|
|
142
|
+
},
|
|
143
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Set translator expectations
|
|
147
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_EMAIL', {})
|
|
148
|
+
|
|
149
|
+
expect(r$.email.$rules.email.$message).toBe('VALIDATE.INVALID_EMAIL') // Custom message
|
|
150
|
+
expect(r$.email2.$rules.email.$message).toBe('This field is not valid') // Default message
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('length rule', () => {
|
|
154
|
+
const yamlInput = `
|
|
155
|
+
tooShort:
|
|
156
|
+
validators:
|
|
157
|
+
length:
|
|
158
|
+
min: 5
|
|
159
|
+
tooLong:
|
|
160
|
+
validators:
|
|
161
|
+
length:
|
|
162
|
+
max: 2
|
|
163
|
+
tooShortWithMessage:
|
|
164
|
+
validators:
|
|
165
|
+
length:
|
|
166
|
+
min: 5
|
|
167
|
+
message: VALIDATE.LENGTH_RANGE
|
|
168
|
+
tooLongWithMessage:
|
|
169
|
+
validators:
|
|
170
|
+
length:
|
|
171
|
+
max: 2
|
|
172
|
+
message: VALIDATE.LENGTH_RANGE
|
|
173
|
+
`
|
|
174
|
+
|
|
175
|
+
const { r$ } = useRegle(
|
|
176
|
+
{
|
|
177
|
+
tooShort: '1',
|
|
178
|
+
tooLong: '123',
|
|
179
|
+
tooShortWithMessage: '1',
|
|
180
|
+
tooLongWithMessage: '123'
|
|
181
|
+
},
|
|
182
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Set translator expectations
|
|
186
|
+
expect(translateMock).toHaveBeenNthCalledWith(1, 'VALIDATE.LENGTH_RANGE', { min: 5 })
|
|
187
|
+
expect(translateMock).toHaveBeenNthCalledWith(2, 'VALIDATE.LENGTH_RANGE', { max: 2 })
|
|
188
|
+
|
|
189
|
+
expect(r$.tooShort.$silentErrors).toEqual(['The value length should be at least 5']) // Custom message
|
|
190
|
+
expect(r$.tooLong.$silentErrors).toEqual(['The value length should not exceed 2']) // Default message
|
|
191
|
+
expect(r$.tooShortWithMessage.$silentErrors).toEqual(['VALIDATE.LENGTH_RANGE']) // Custom message
|
|
192
|
+
expect(r$.tooLongWithMessage.$silentErrors).toEqual(['VALIDATE.LENGTH_RANGE']) // Default message
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('integer rule', () => {
|
|
196
|
+
const yamlInput = `
|
|
197
|
+
foo:
|
|
198
|
+
validators:
|
|
199
|
+
integer: true
|
|
200
|
+
bar:
|
|
201
|
+
validators:
|
|
202
|
+
integer:
|
|
203
|
+
message: VALIDATE.INVALID_INTEGER
|
|
204
|
+
foobar:
|
|
205
|
+
validators:
|
|
206
|
+
integer: true
|
|
207
|
+
`
|
|
208
|
+
|
|
209
|
+
const { r$ } = useRegle(
|
|
210
|
+
{
|
|
211
|
+
foo: 'one',
|
|
212
|
+
bar: 'two',
|
|
213
|
+
foobar: 92
|
|
214
|
+
},
|
|
215
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// Set translator expectations
|
|
219
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_INTEGER', {})
|
|
220
|
+
|
|
221
|
+
expect(r$.foo.$silentErrors).toEqual(['The value must be an integer']) // Custom message
|
|
222
|
+
expect(r$.bar.$silentErrors).toEqual(['VALIDATE.INVALID_INTEGER']) // Default message
|
|
223
|
+
expect(r$.foobar.$silentErrors).toEqual([]) // Valid
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('member_of rule', () => {
|
|
227
|
+
const yamlInput = `
|
|
228
|
+
genus:
|
|
229
|
+
validators:
|
|
230
|
+
member_of:
|
|
231
|
+
values:
|
|
232
|
+
- Megascops
|
|
233
|
+
- Bubo
|
|
234
|
+
- Glaucidium
|
|
235
|
+
- Tyto
|
|
236
|
+
- Athene
|
|
237
|
+
message: Sorry, that is not one of the permitted genuses.
|
|
238
|
+
owls:
|
|
239
|
+
validators:
|
|
240
|
+
member_of:
|
|
241
|
+
values:
|
|
242
|
+
- Foo
|
|
243
|
+
- Bar
|
|
244
|
+
valid:
|
|
245
|
+
validators:
|
|
246
|
+
member_of:
|
|
247
|
+
values:
|
|
248
|
+
- Foo
|
|
249
|
+
- Bar
|
|
250
|
+
`
|
|
251
|
+
|
|
252
|
+
const { r$ } = useRegle(
|
|
253
|
+
{
|
|
254
|
+
genus: 'Foo',
|
|
255
|
+
owls: 'Hedwig',
|
|
256
|
+
valid: 'Foo'
|
|
257
|
+
},
|
|
258
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
// Set translator expectations
|
|
262
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith(
|
|
263
|
+
'Sorry, that is not one of the permitted genuses.',
|
|
264
|
+
{
|
|
265
|
+
values: ['Megascops', 'Bubo', 'Glaucidium', 'Tyto', 'Athene']
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
expect(r$.genus.$silentErrors).toEqual(['Sorry, that is not one of the permitted genuses.']) // Custom message
|
|
270
|
+
expect(r$.owls.$silentErrors).toEqual([
|
|
271
|
+
'The value should be one of those options: Foo, Bar.'
|
|
272
|
+
]) // Default message
|
|
273
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('not_member_of rule', () => {
|
|
277
|
+
const yamlInput = `
|
|
278
|
+
genus:
|
|
279
|
+
validators:
|
|
280
|
+
not_member_of:
|
|
281
|
+
values:
|
|
282
|
+
- Megascops
|
|
283
|
+
- Bubo
|
|
284
|
+
- Glaucidium
|
|
285
|
+
- Tyto
|
|
286
|
+
- Athene
|
|
287
|
+
message: VALIDATE.NOT_MEMBER_OF
|
|
288
|
+
owls:
|
|
289
|
+
validators:
|
|
290
|
+
not_member_of:
|
|
291
|
+
values:
|
|
292
|
+
- Foo
|
|
293
|
+
- Bar
|
|
294
|
+
valid:
|
|
295
|
+
validators:
|
|
296
|
+
not_member_of:
|
|
297
|
+
values:
|
|
298
|
+
- Foo
|
|
299
|
+
- Bar
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
const { r$ } = useRegle(
|
|
303
|
+
{
|
|
304
|
+
genus: 'Megascops',
|
|
305
|
+
owls: 'Foo',
|
|
306
|
+
valid: 'Hedwig'
|
|
307
|
+
},
|
|
308
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
// Set translator expectations
|
|
312
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.NOT_MEMBER_OF', {
|
|
313
|
+
values: ['Megascops', 'Bubo', 'Glaucidium', 'Tyto', 'Athene']
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
expect(r$.genus.$silentErrors).toEqual(['VALIDATE.NOT_MEMBER_OF']) // Custom message
|
|
317
|
+
expect(r$.owls.$silentErrors).toEqual(['Error']) // Default message
|
|
318
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('no_leading_whitespace rule', () => {
|
|
322
|
+
const yamlInput = `
|
|
323
|
+
withMessage:
|
|
324
|
+
validators:
|
|
325
|
+
no_leading_whitespace:
|
|
326
|
+
label: "&USERNAME"
|
|
327
|
+
message: VALIDATE.NO_LEAD_WS
|
|
328
|
+
defaultMessage:
|
|
329
|
+
validators:
|
|
330
|
+
no_leading_whitespace: true
|
|
331
|
+
valid:
|
|
332
|
+
validators:
|
|
333
|
+
no_leading_whitespace: true
|
|
334
|
+
`
|
|
335
|
+
|
|
336
|
+
const { r$ } = useRegle(
|
|
337
|
+
{
|
|
338
|
+
withMessage: ' Foo',
|
|
339
|
+
defaultMessage: ' Foo',
|
|
340
|
+
valid: 'Foo'
|
|
341
|
+
},
|
|
342
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// Set translator expectations
|
|
346
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.NO_LEAD_WS', {
|
|
347
|
+
label: '&USERNAME'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.NO_LEAD_WS']) // Custom message
|
|
351
|
+
expect(r$.defaultMessage.$silentErrors).toEqual([
|
|
352
|
+
'The value does not match the required pattern'
|
|
353
|
+
]) // Default message
|
|
354
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('no_trailing_whitespace rule', () => {
|
|
358
|
+
const yamlInput = `
|
|
359
|
+
withMessage:
|
|
360
|
+
validators:
|
|
361
|
+
no_trailing_whitespace:
|
|
362
|
+
label: "&USERNAME"
|
|
363
|
+
message: VALIDATE.NO_TRAIL_WS
|
|
364
|
+
defaultMessage:
|
|
365
|
+
validators:
|
|
366
|
+
no_trailing_whitespace: true
|
|
367
|
+
valid:
|
|
368
|
+
validators:
|
|
369
|
+
no_trailing_whitespace: true
|
|
370
|
+
`
|
|
371
|
+
|
|
372
|
+
const { r$ } = useRegle(
|
|
373
|
+
{
|
|
374
|
+
withMessage: 'Foo ',
|
|
375
|
+
defaultMessage: 'Foo ',
|
|
376
|
+
valid: 'Foo'
|
|
377
|
+
},
|
|
378
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
// Set translator expectations
|
|
382
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.NO_TRAIL_WS', {
|
|
383
|
+
label: '&USERNAME'
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.NO_TRAIL_WS']) // Custom message
|
|
387
|
+
expect(r$.defaultMessage.$silentErrors).toEqual([
|
|
388
|
+
'The value does not match the required pattern'
|
|
389
|
+
]) // Default message
|
|
390
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('numeric rule', () => {
|
|
394
|
+
const yamlInput = `
|
|
395
|
+
withMessage:
|
|
396
|
+
validators:
|
|
397
|
+
numeric:
|
|
398
|
+
message: VALIDATE.INVALID_NUMERIC
|
|
399
|
+
defaultMessage:
|
|
400
|
+
validators:
|
|
401
|
+
numeric: true
|
|
402
|
+
valid:
|
|
403
|
+
validators:
|
|
404
|
+
numeric: true
|
|
405
|
+
`
|
|
406
|
+
|
|
407
|
+
const { r$ } = useRegle(
|
|
408
|
+
{
|
|
409
|
+
withMessage: 'Foo',
|
|
410
|
+
defaultMessage: 'Foo',
|
|
411
|
+
valid: '10.2'
|
|
412
|
+
},
|
|
413
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
// Set translator expectations
|
|
417
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_NUMERIC', {})
|
|
418
|
+
|
|
419
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.INVALID_NUMERIC']) // Custom message
|
|
420
|
+
expect(r$.defaultMessage.$silentErrors).toEqual(['The value must be numeric']) // Default message
|
|
421
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('range rule', () => {
|
|
425
|
+
const yamlInput = `
|
|
426
|
+
withMessage:
|
|
427
|
+
validators:
|
|
428
|
+
range:
|
|
429
|
+
min: 0
|
|
430
|
+
max: 10
|
|
431
|
+
message: VALIDATE.INVALID_RANGE
|
|
432
|
+
defaultMessage:
|
|
433
|
+
validators:
|
|
434
|
+
range:
|
|
435
|
+
min: 0
|
|
436
|
+
max: 10
|
|
437
|
+
valid:
|
|
438
|
+
validators:
|
|
439
|
+
range:
|
|
440
|
+
min: 0
|
|
441
|
+
max: 10
|
|
442
|
+
`
|
|
443
|
+
|
|
444
|
+
const { r$ } = useRegle(
|
|
445
|
+
{
|
|
446
|
+
withMessage: 92,
|
|
447
|
+
defaultMessage: 92,
|
|
448
|
+
valid: 9
|
|
449
|
+
},
|
|
450
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
// Set translator expectations
|
|
454
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_RANGE', {
|
|
455
|
+
min: 0,
|
|
456
|
+
max: 10
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.INVALID_RANGE']) // Custom message
|
|
460
|
+
expect(r$.defaultMessage.$silentErrors).toEqual(['The value must be between 0 and 10']) // Default message
|
|
461
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
test('regex rule', () => {
|
|
465
|
+
const yamlInput = `
|
|
466
|
+
withMessage:
|
|
467
|
+
validators:
|
|
468
|
+
regex:
|
|
469
|
+
regex: ^who(o*)$
|
|
470
|
+
message: VALIDATE.INVALID_VALUE
|
|
471
|
+
defaultMessage:
|
|
472
|
+
validators:
|
|
473
|
+
regex:
|
|
474
|
+
regex: ^who(o*)$
|
|
475
|
+
valid:
|
|
476
|
+
validators:
|
|
477
|
+
regex:
|
|
478
|
+
regex: ^who(o*)$
|
|
479
|
+
`
|
|
480
|
+
|
|
481
|
+
const { r$ } = useRegle(
|
|
482
|
+
{
|
|
483
|
+
withMessage: 'hum',
|
|
484
|
+
defaultMessage: 'hello',
|
|
485
|
+
valid: 'whooo'
|
|
486
|
+
},
|
|
487
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
488
|
+
)
|
|
489
|
+
r$.$validate()
|
|
490
|
+
|
|
491
|
+
// Set translator expectations
|
|
492
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_VALUE', {
|
|
493
|
+
regex: '^who(o*)$'
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
expect(r$.withMessage.$errors).toEqual(['VALIDATE.INVALID_VALUE']) // Custom message
|
|
497
|
+
expect(r$.defaultMessage.$errors).toEqual(['The value does not match the required pattern']) // Default message
|
|
498
|
+
expect(r$.valid.$correct).toEqual(true) // Valid
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('uri rule', () => {
|
|
502
|
+
const yamlInput = `
|
|
503
|
+
withMessage:
|
|
504
|
+
validators:
|
|
505
|
+
uri:
|
|
506
|
+
message: VALIDATE.INVALID_URL
|
|
507
|
+
defaultMessage:
|
|
508
|
+
validators:
|
|
509
|
+
uri: true
|
|
510
|
+
valid:
|
|
511
|
+
validators:
|
|
512
|
+
uri: true
|
|
513
|
+
`
|
|
514
|
+
|
|
515
|
+
const { r$ } = useRegle(
|
|
516
|
+
{
|
|
517
|
+
withMessage: 'foo',
|
|
518
|
+
defaultMessage: 'bar@example.com',
|
|
519
|
+
valid: 'http://example.com'
|
|
520
|
+
},
|
|
521
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
// Set translator expectations
|
|
525
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_URL', {})
|
|
526
|
+
|
|
527
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.INVALID_URL']) // Custom message
|
|
528
|
+
expect(r$.defaultMessage.$silentErrors).toEqual(['The value is not a valid URL address']) // Default message
|
|
529
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
test('username rule', () => {
|
|
533
|
+
const yamlInput = `
|
|
534
|
+
withMessage:
|
|
535
|
+
validators:
|
|
536
|
+
username:
|
|
537
|
+
message: VALIDATE.INVALID_USERNAME
|
|
538
|
+
defaultMessage:
|
|
539
|
+
validators:
|
|
540
|
+
username: true
|
|
541
|
+
valid:
|
|
542
|
+
validators:
|
|
543
|
+
username: true
|
|
544
|
+
`
|
|
545
|
+
|
|
546
|
+
const { r$ } = useRegle(
|
|
547
|
+
{
|
|
548
|
+
withMessage: 'My Name',
|
|
549
|
+
defaultMessage: 'bar@example.com',
|
|
550
|
+
valid: 'foo.bar-bax_123'
|
|
551
|
+
},
|
|
552
|
+
useRuleSchemaAdapter().adapt(yamlInput)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
// Set translator expectations
|
|
556
|
+
expect(translateMock).toHaveBeenCalledExactlyOnceWith('VALIDATE.INVALID_USERNAME', {})
|
|
557
|
+
|
|
558
|
+
expect(r$.withMessage.$silentErrors).toEqual(['VALIDATE.INVALID_USERNAME']) // Custom message
|
|
559
|
+
expect(r$.defaultMessage.$silentErrors).toEqual([
|
|
560
|
+
'The value does not match the required pattern'
|
|
561
|
+
]) // Default message
|
|
562
|
+
expect(r$.valid.$silentErrors).toEqual([]) // Valid
|
|
563
|
+
})
|
|
564
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test, vi } from 'vitest'
|
|
2
2
|
import { createApp } from 'vue'
|
|
3
|
-
import { useConfigStore } from '../stores/
|
|
3
|
+
import { useConfigStore } from '../stores/useConfigStore'
|
|
4
4
|
import plugin from '..'
|
|
5
|
-
import * as Config from '../stores/
|
|
5
|
+
import * as Config from '../stores/useConfigStore'
|
|
6
6
|
import * as Translator from '../stores/useTranslator'
|
|
7
7
|
|
|
8
8
|
const mockConfigStore = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, beforeEach, test, vi } from 'vitest'
|
|
2
2
|
import { setActivePinia, createPinia } from 'pinia'
|
|
3
3
|
import axios from 'axios'
|
|
4
|
-
import { useConfigStore } from '../../stores/
|
|
4
|
+
import { useConfigStore } from '../../stores/useConfigStore'
|
|
5
5
|
|
|
6
6
|
const testConfig = {
|
|
7
7
|
name: 'Test Config',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@userfrosting/sprinkle-core",
|
|
3
|
-
"version": "6.0.0-
|
|
3
|
+
"version": "6.0.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core Sprinkle for UserFrosting",
|
|
6
6
|
"funding": "https://opencollective.com/userfrosting",
|
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
"app/assets/"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@regle/core": "^1.6.0",
|
|
38
|
+
"@regle/rules": "^1.6.0",
|
|
37
39
|
"dot-prop": "^9.0.0",
|
|
38
|
-
"luxon": "^3.5.0"
|
|
40
|
+
"luxon": "^3.5.0",
|
|
41
|
+
"yaml": "^2.8.0"
|
|
39
42
|
},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"axios": "^1.5.0",
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
"happy-dom": "^15.11.6",
|
|
60
63
|
"less": "^4.2.0",
|
|
61
64
|
"npm-run-all2": "^6.1.2",
|
|
62
|
-
"prettier": "^3.2
|
|
65
|
+
"prettier": "^3.6.2",
|
|
63
66
|
"vite": "^6.2",
|
|
64
67
|
"vite-plugin-dts": "^4.0.0",
|
|
65
68
|
"vitest": "^3.1.1",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|