@pyreon/feature 0.10.0 → 0.11.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/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +18 -6
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +20 -12
- package/src/define-feature.ts +71 -104
- package/src/index.ts +5 -4
- package/src/schema.ts +59 -63
- package/src/tests/feature.test.tsx +408 -473
- package/src/types.ts +25 -14
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { defineFeature } from
|
|
2
|
-
export type { FieldInfo, FieldType, ReferenceSchema } from
|
|
1
|
+
export { defineFeature } from "./define-feature"
|
|
2
|
+
export type { FieldInfo, FieldType, ReferenceSchema } from "./schema"
|
|
3
3
|
export {
|
|
4
4
|
defaultInitialValues,
|
|
5
5
|
extractFields,
|
|
6
6
|
isReference,
|
|
7
7
|
reference,
|
|
8
|
-
} from
|
|
8
|
+
} from "./schema"
|
|
9
9
|
export type {
|
|
10
10
|
Feature,
|
|
11
11
|
FeatureConfig,
|
|
@@ -13,5 +13,6 @@ export type {
|
|
|
13
13
|
FeatureStore,
|
|
14
14
|
FeatureTableOptions,
|
|
15
15
|
FeatureTableResult,
|
|
16
|
+
InferSchemaValues,
|
|
16
17
|
ListOptions,
|
|
17
|
-
} from
|
|
18
|
+
} from "./types"
|
package/src/schema.ts
CHANGED
|
@@ -21,18 +21,18 @@ export interface FieldInfo {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export type FieldType =
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
24
|
+
| "string"
|
|
25
|
+
| "number"
|
|
26
|
+
| "boolean"
|
|
27
|
+
| "date"
|
|
28
|
+
| "enum"
|
|
29
|
+
| "array"
|
|
30
|
+
| "object"
|
|
31
|
+
| "reference"
|
|
32
|
+
| "unknown"
|
|
33
33
|
|
|
34
34
|
/** Symbol used to tag reference schema objects. */
|
|
35
|
-
const REFERENCE_TAG = Symbol.for(
|
|
35
|
+
const REFERENCE_TAG = Symbol.for("pyreon:feature:reference")
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Metadata carried by a reference schema.
|
|
@@ -61,7 +61,7 @@ export interface ReferenceSchema {
|
|
|
61
61
|
export function isReference(value: unknown): value is ReferenceSchema {
|
|
62
62
|
return (
|
|
63
63
|
value !== null &&
|
|
64
|
-
typeof value ===
|
|
64
|
+
typeof value === "object" &&
|
|
65
65
|
(value as Record<symbol, unknown>)[REFERENCE_TAG] === true
|
|
66
66
|
)
|
|
67
67
|
}
|
|
@@ -93,7 +93,7 @@ export function reference(feature: { name: string }): ReferenceSchema {
|
|
|
93
93
|
success: boolean
|
|
94
94
|
error?: { issues: { message: string }[] }
|
|
95
95
|
} {
|
|
96
|
-
if (typeof value ===
|
|
96
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
97
97
|
return { success: true }
|
|
98
98
|
}
|
|
99
99
|
return {
|
|
@@ -113,7 +113,7 @@ export function reference(feature: { name: string }): ReferenceSchema {
|
|
|
113
113
|
_featureName: featureName,
|
|
114
114
|
safeParse: validateRef,
|
|
115
115
|
safeParseAsync: async (value: unknown) => validateRef(value),
|
|
116
|
-
_def: { typeName:
|
|
116
|
+
_def: { typeName: "ZodString" },
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -123,8 +123,8 @@ export function reference(feature: { name: string }): ReferenceSchema {
|
|
|
123
123
|
*/
|
|
124
124
|
function nameToLabel(name: string): string {
|
|
125
125
|
return name
|
|
126
|
-
.replace(/([a-z])([A-Z])/g,
|
|
127
|
-
.replace(/[_-]/g,
|
|
126
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase → camel Case
|
|
127
|
+
.replace(/[_-]/g, " ") // snake_case/kebab-case → spaces
|
|
128
128
|
.replace(/\b\w/g, (c) => c.toUpperCase()) // capitalize words
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -141,14 +141,14 @@ function detectFieldType(zodField: unknown): {
|
|
|
141
141
|
// Check for reference fields first
|
|
142
142
|
if (isReference(zodField)) {
|
|
143
143
|
return {
|
|
144
|
-
type:
|
|
144
|
+
type: "reference",
|
|
145
145
|
optional: false,
|
|
146
146
|
referenceTo: zodField._featureName,
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
if (!zodField || typeof zodField !==
|
|
151
|
-
return { type:
|
|
150
|
+
if (!zodField || typeof zodField !== "object") {
|
|
151
|
+
return { type: "unknown", optional: false }
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
const field = zodField as Record<string, unknown>
|
|
@@ -161,13 +161,13 @@ function detectFieldType(zodField: unknown): {
|
|
|
161
161
|
const getTypeName = (obj: Record<string, unknown>): string | undefined => {
|
|
162
162
|
// v3 path
|
|
163
163
|
const def = obj._def as Record<string, unknown> | undefined
|
|
164
|
-
if (def?.typeName && typeof def.typeName ===
|
|
164
|
+
if (def?.typeName && typeof def.typeName === "string") {
|
|
165
165
|
return def.typeName
|
|
166
166
|
}
|
|
167
167
|
// v4 path
|
|
168
168
|
const zod = obj._zod as Record<string, unknown> | undefined
|
|
169
169
|
const zodDef = zod?.def as Record<string, unknown> | undefined
|
|
170
|
-
if (zodDef?.type && typeof zodDef.type ===
|
|
170
|
+
if (zodDef?.type && typeof zodDef.type === "string") {
|
|
171
171
|
return zodDef.type
|
|
172
172
|
}
|
|
173
173
|
return undefined
|
|
@@ -177,16 +177,15 @@ function detectFieldType(zodField: unknown): {
|
|
|
177
177
|
|
|
178
178
|
// Unwrap optional/nullable
|
|
179
179
|
if (
|
|
180
|
-
typeName ===
|
|
181
|
-
typeName ===
|
|
182
|
-
typeName ===
|
|
183
|
-
typeName ===
|
|
180
|
+
typeName === "ZodOptional" ||
|
|
181
|
+
typeName === "ZodNullable" ||
|
|
182
|
+
typeName === "optional" ||
|
|
183
|
+
typeName === "nullable"
|
|
184
184
|
) {
|
|
185
185
|
optional = true
|
|
186
186
|
const def = inner._def as Record<string, unknown> | undefined
|
|
187
|
-
const innerType =
|
|
188
|
-
|
|
189
|
-
if (innerType && typeof innerType === 'object') {
|
|
187
|
+
const innerType = def?.innerType ?? (inner._zod as Record<string, unknown>)?.def
|
|
188
|
+
if (innerType && typeof innerType === "object") {
|
|
190
189
|
inner = innerType as Record<string, unknown>
|
|
191
190
|
}
|
|
192
191
|
}
|
|
@@ -194,32 +193,32 @@ function detectFieldType(zodField: unknown): {
|
|
|
194
193
|
const innerTypeName = getTypeName(inner) ?? typeName
|
|
195
194
|
|
|
196
195
|
// Map Zod type names to our FieldType
|
|
197
|
-
if (!innerTypeName) return { type:
|
|
196
|
+
if (!innerTypeName) return { type: "unknown", optional }
|
|
198
197
|
|
|
199
198
|
const typeMap: Record<string, FieldType> = {
|
|
200
|
-
ZodString:
|
|
201
|
-
ZodNumber:
|
|
202
|
-
ZodBoolean:
|
|
203
|
-
ZodDate:
|
|
204
|
-
ZodEnum:
|
|
205
|
-
ZodNativeEnum:
|
|
206
|
-
ZodArray:
|
|
207
|
-
ZodObject:
|
|
199
|
+
ZodString: "string",
|
|
200
|
+
ZodNumber: "number",
|
|
201
|
+
ZodBoolean: "boolean",
|
|
202
|
+
ZodDate: "date",
|
|
203
|
+
ZodEnum: "enum",
|
|
204
|
+
ZodNativeEnum: "enum",
|
|
205
|
+
ZodArray: "array",
|
|
206
|
+
ZodObject: "object",
|
|
208
207
|
// v4 names
|
|
209
|
-
string:
|
|
210
|
-
number:
|
|
211
|
-
boolean:
|
|
212
|
-
date:
|
|
213
|
-
enum:
|
|
214
|
-
array:
|
|
215
|
-
object:
|
|
208
|
+
string: "string",
|
|
209
|
+
number: "number",
|
|
210
|
+
boolean: "boolean",
|
|
211
|
+
date: "date",
|
|
212
|
+
enum: "enum",
|
|
213
|
+
array: "array",
|
|
214
|
+
object: "object",
|
|
216
215
|
}
|
|
217
216
|
|
|
218
|
-
const type = typeMap[innerTypeName] ??
|
|
217
|
+
const type = typeMap[innerTypeName] ?? "string"
|
|
219
218
|
|
|
220
219
|
// Extract enum values
|
|
221
220
|
let enumValues: (string | number)[] | undefined
|
|
222
|
-
if (type ===
|
|
221
|
+
if (type === "enum") {
|
|
223
222
|
const def = inner._def as Record<string, unknown> | undefined
|
|
224
223
|
if (def?.values && Array.isArray(def.values)) {
|
|
225
224
|
enumValues = def.values as (string | number)[]
|
|
@@ -255,7 +254,7 @@ function detectFieldType(zodField: unknown): {
|
|
|
255
254
|
* ```
|
|
256
255
|
*/
|
|
257
256
|
export function extractFields(schema: unknown): FieldInfo[] {
|
|
258
|
-
if (!schema || typeof schema !==
|
|
257
|
+
if (!schema || typeof schema !== "object") return []
|
|
259
258
|
|
|
260
259
|
const s = schema as Record<string, unknown>
|
|
261
260
|
|
|
@@ -265,7 +264,7 @@ export function extractFields(schema: unknown): FieldInfo[] {
|
|
|
265
264
|
let shape: Record<string, unknown> | undefined
|
|
266
265
|
|
|
267
266
|
// Try schema.shape (works for both v3 and v4)
|
|
268
|
-
if (s.shape && typeof s.shape ===
|
|
267
|
+
if (s.shape && typeof s.shape === "object") {
|
|
269
268
|
shape = s.shape as Record<string, unknown>
|
|
270
269
|
}
|
|
271
270
|
|
|
@@ -274,7 +273,7 @@ export function extractFields(schema: unknown): FieldInfo[] {
|
|
|
274
273
|
const def = s._def as Record<string, unknown> | undefined
|
|
275
274
|
if (def?.shape) {
|
|
276
275
|
shape =
|
|
277
|
-
typeof def.shape ===
|
|
276
|
+
typeof def.shape === "function"
|
|
278
277
|
? (def.shape as () => Record<string, unknown>)()
|
|
279
278
|
: (def.shape as Record<string, unknown>)
|
|
280
279
|
}
|
|
@@ -284,7 +283,7 @@ export function extractFields(schema: unknown): FieldInfo[] {
|
|
|
284
283
|
if (!shape) {
|
|
285
284
|
const zod = s._zod as Record<string, unknown> | undefined
|
|
286
285
|
const zodDef = zod?.def as Record<string, unknown> | undefined
|
|
287
|
-
if (zodDef?.shape && typeof zodDef.shape ===
|
|
286
|
+
if (zodDef?.shape && typeof zodDef.shape === "object") {
|
|
288
287
|
shape = zodDef.shape as Record<string, unknown>
|
|
289
288
|
}
|
|
290
289
|
}
|
|
@@ -292,8 +291,7 @@ export function extractFields(schema: unknown): FieldInfo[] {
|
|
|
292
291
|
if (!shape) return []
|
|
293
292
|
|
|
294
293
|
return Object.entries(shape).map(([name, fieldSchema]) => {
|
|
295
|
-
const { type, optional, enumValues, referenceTo } =
|
|
296
|
-
detectFieldType(fieldSchema)
|
|
294
|
+
const { type, optional, enumValues, referenceTo } = detectFieldType(fieldSchema)
|
|
297
295
|
const info: FieldInfo = {
|
|
298
296
|
name,
|
|
299
297
|
type,
|
|
@@ -309,29 +307,27 @@ export function extractFields(schema: unknown): FieldInfo[] {
|
|
|
309
307
|
/**
|
|
310
308
|
* Generate default initial values from a schema's field types.
|
|
311
309
|
*/
|
|
312
|
-
export function defaultInitialValues(
|
|
313
|
-
fields: FieldInfo[],
|
|
314
|
-
): Record<string, unknown> {
|
|
310
|
+
export function defaultInitialValues(fields: FieldInfo[]): Record<string, unknown> {
|
|
315
311
|
const values: Record<string, unknown> = {}
|
|
316
312
|
for (const field of fields) {
|
|
317
313
|
switch (field.type) {
|
|
318
|
-
case
|
|
319
|
-
values[field.name] =
|
|
314
|
+
case "string":
|
|
315
|
+
values[field.name] = ""
|
|
320
316
|
break
|
|
321
|
-
case
|
|
317
|
+
case "number":
|
|
322
318
|
values[field.name] = 0
|
|
323
319
|
break
|
|
324
|
-
case
|
|
320
|
+
case "boolean":
|
|
325
321
|
values[field.name] = false
|
|
326
322
|
break
|
|
327
|
-
case
|
|
328
|
-
values[field.name] = field.enumValues?.[0] ??
|
|
323
|
+
case "enum":
|
|
324
|
+
values[field.name] = field.enumValues?.[0] ?? ""
|
|
329
325
|
break
|
|
330
|
-
case
|
|
331
|
-
values[field.name] =
|
|
326
|
+
case "date":
|
|
327
|
+
values[field.name] = ""
|
|
332
328
|
break
|
|
333
329
|
default:
|
|
334
|
-
values[field.name] =
|
|
330
|
+
values[field.name] = ""
|
|
335
331
|
}
|
|
336
332
|
}
|
|
337
333
|
return values
|