0.1.15 (#16)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@thilawyn/thilalib",
|
"name": "@thilawyn/thilalib",
|
||||||
"version": "0.1.14",
|
"version": "0.1.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./dist"
|
"./dist"
|
||||||
@@ -47,6 +47,16 @@
|
|||||||
"default": "./dist/Schema/MobX/index.cjs"
|
"default": "./dist/Schema/MobX/index.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"./Schema/TanStackForm": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/Schema/TanStackForm/index.d.ts",
|
||||||
|
"default": "./dist/Schema/TanStackForm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/Schema/TanStackForm/index.d.cts",
|
||||||
|
"default": "./dist/Schema/TanStackForm/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"./Types": {
|
"./Types": {
|
||||||
"import": {
|
"import": {
|
||||||
"types": "./dist/Types/index.d.ts",
|
"types": "./dist/Types/index.d.ts",
|
||||||
@@ -66,20 +76,22 @@
|
|||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"remeda": "^2.11.0",
|
"remeda": "^2.12.0",
|
||||||
"type-fest": "^4.25.0"
|
"type-fest": "^4.26.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/schema": "^0.71.1",
|
"@effect/schema": "^0.72.0",
|
||||||
|
"@prisma/studio-server": "^0.502.0",
|
||||||
|
"@tanstack/form-core": "^0.30.0",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"bun-types": "^1.1.26",
|
"bun-types": "^1.1.26",
|
||||||
"effect": "^3.6.5",
|
"effect": "^3.7.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mobx": "^6.13.1",
|
"mobx": "^6.13.1",
|
||||||
"npm-check-updates": "^17.1.0",
|
"npm-check-updates": "^17.1.1",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.2.4",
|
||||||
"tsx": "^4.18.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/Layers/PrismaStudioRoute.ts
Normal file
44
src/Layers/PrismaStudioRoute.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// import { StudioServer } from "@prisma/studio-server"
|
||||||
|
// import { Config, Effect, Layer } from "effect"
|
||||||
|
// import proxy from "express-http-proxy"
|
||||||
|
// import { ExpressApp } from "../http/ExpressApp.service"
|
||||||
|
// import { PrismaClient } from "./PrismaClient.service"
|
||||||
|
|
||||||
|
|
||||||
|
// export const PrismaStudioRoute = ({
|
||||||
|
// httpRoot = Config.succeed("/studio"),
|
||||||
|
// httpPort = Config.succeed(5555),
|
||||||
|
// schemaPath = Config.succeed(""),
|
||||||
|
// schemaText = Config.succeed(""),
|
||||||
|
// }: {
|
||||||
|
// readonly httpRoot?: Config.Config<string>
|
||||||
|
// readonly httpPort?: Config.Config<number>
|
||||||
|
// readonly schemaPath?: Config.Config<string>
|
||||||
|
// readonly schemaText?: Config.Config<string>
|
||||||
|
// } = {}) =>
|
||||||
|
// Layer.effectDiscard(Effect.acquireRelease(
|
||||||
|
// Effect.gen(function*() {
|
||||||
|
// const prisma = yield* PrismaClient
|
||||||
|
// const app = yield* ExpressApp
|
||||||
|
|
||||||
|
// const port = yield* httpPort
|
||||||
|
|
||||||
|
// const server = new StudioServer({
|
||||||
|
// port,
|
||||||
|
// schemaPath: yield* schemaPath,
|
||||||
|
// schemaText: yield* schemaText,
|
||||||
|
// versions: { prisma: prisma.Prisma.prismaVersion.client },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// app.use(yield* httpRoot, proxy(`http://localhost:${ port }`))
|
||||||
|
|
||||||
|
// yield* Effect.promise(() => server.start())
|
||||||
|
// return server
|
||||||
|
// }),
|
||||||
|
|
||||||
|
// server => Effect.sync(() => server.stop()),
|
||||||
|
// ))
|
||||||
|
|
||||||
|
|
||||||
|
// export const PrismaStudioRouteLive = Layer.empty
|
||||||
|
// export const PrismaStudioRouteDebug = PrismaStudioRoute()
|
||||||
12
src/Schema/Email.ts
Normal file
12
src/Schema/Email.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Schema } from "@effect/schema"
|
||||||
|
|
||||||
|
|
||||||
|
export const Email = Schema.pattern(
|
||||||
|
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
|
||||||
|
|
||||||
|
{
|
||||||
|
identifier: "Email",
|
||||||
|
title: "email",
|
||||||
|
message: () => "Not an email address",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -8,19 +8,19 @@ interface ObservableClassSelf {
|
|||||||
readonly fields: { readonly [K in keyof Schema.Struct.Fields]: Schema.Struct.Fields[K] }
|
readonly fields: { readonly [K in keyof Schema.Struct.Fields]: Schema.Struct.Fields[K] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ObservableClass = <Self extends ObservableClassSelf>(
|
interface ObservableClassOptions extends Omit<CreateObservableOptions, "proxy"> {}
|
||||||
self: Self,
|
|
||||||
options?: Omit<CreateObservableOptions, "proxy">,
|
|
||||||
) =>
|
|
||||||
class Observable extends self {
|
|
||||||
declare ["constructor"]: typeof Observable
|
|
||||||
|
|
||||||
constructor(...args: any[]) {
|
export const ObservableClass = (options?: ObservableClassOptions) =>
|
||||||
super(...args)
|
<Self extends ObservableClassSelf>(self: Self) =>
|
||||||
|
class Observable extends self {
|
||||||
|
declare ["constructor"]: typeof Observable
|
||||||
|
|
||||||
makeObservable(this,
|
constructor(...args: any[]) {
|
||||||
mapValues(this.constructor.fields, () => observable),
|
super(...args)
|
||||||
options,
|
|
||||||
)
|
makeObservable(this,
|
||||||
}
|
mapValues(this.constructor.fields, () => observable),
|
||||||
} as Self
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} as Self
|
||||||
|
|||||||
1
src/Schema/TanStackForm/index.ts
Normal file
1
src/Schema/TanStackForm/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./makeSchemaFormValidator"
|
||||||
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal file
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { ArrayFormatter, Schema } from "@effect/schema"
|
||||||
|
import type { ParseOptions } from "@effect/schema/AST"
|
||||||
|
import type { DeepKeys, DeepValue, FieldApi, FormApi, FormValidationError, ValidationError, Validator } from "@tanstack/form-core"
|
||||||
|
import { Array, Effect, Either, flow, Layer, ManagedRuntime, pipe } from "effect"
|
||||||
|
import { mapValues } from "remeda"
|
||||||
|
|
||||||
|
|
||||||
|
export const makeSchemaFormValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const decodeEither = Schema.decodeEither(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TFormData extends I,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined,
|
||||||
|
>(props: {
|
||||||
|
value: TFormData
|
||||||
|
formApi: FormApi<TFormData, TFormValidator>
|
||||||
|
}): FormValidationError<TFormData> =>
|
||||||
|
decodeEither(props.value).pipe(result =>
|
||||||
|
Either.isLeft(result)
|
||||||
|
? {
|
||||||
|
form: "Please check the fields",
|
||||||
|
fields: issuesToFieldsRecord(ArrayFormatter.formatErrorSync(result.left), props.formApi) as any,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSchemaAsyncFormValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const runtime = ManagedRuntime.make(Layer.empty)
|
||||||
|
const decode = Schema.decode(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TFormData extends I,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined,
|
||||||
|
>(props: {
|
||||||
|
value: TFormData
|
||||||
|
formApi: FormApi<TFormData, TFormValidator>
|
||||||
|
signal: AbortSignal
|
||||||
|
}): Promise<FormValidationError<TFormData>> =>
|
||||||
|
decode(props.value).pipe(
|
||||||
|
Effect.matchEffect({
|
||||||
|
onSuccess: () => Effect.succeed(null),
|
||||||
|
|
||||||
|
onFailure: e => ArrayFormatter.formatError(e).pipe(
|
||||||
|
Effect.map(issues => ({
|
||||||
|
form: "Please check the fields",
|
||||||
|
fields: issuesToFieldsRecord(issues, props.formApi) as any,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
prgm => runtime.runPromise(prgm, { signal: props.signal }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: handle nested array fields
|
||||||
|
*/
|
||||||
|
const issuesToFieldsRecord = <
|
||||||
|
TFormData,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
|
||||||
|
>(
|
||||||
|
issues: readonly ArrayFormatter.Issue[],
|
||||||
|
_formApi: FormApi<TFormData, TFormValidator>,
|
||||||
|
) => pipe(issues,
|
||||||
|
Array.groupBy(issue => issue.path.join(".")),
|
||||||
|
|
||||||
|
mapValues(flow(
|
||||||
|
Array.map(issue => issue.message),
|
||||||
|
Array.join("\n"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const makeSchemaFieldValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const decodeEither = Schema.decodeEither(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
|
||||||
|
TFormValidator extends Validator<TParentData, unknown> | undefined,
|
||||||
|
TData extends I & DeepValue<TParentData, TName>,
|
||||||
|
>(props: {
|
||||||
|
value: TData
|
||||||
|
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
|
||||||
|
}): ValidationError =>
|
||||||
|
decodeEither(props.value).pipe(result =>
|
||||||
|
Either.isLeft(result)
|
||||||
|
? ArrayFormatter.formatErrorSync(result.left)
|
||||||
|
.map(issue => issue.message)
|
||||||
|
.join("\n")
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSchemaAsyncFieldValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const runtime = ManagedRuntime.make(Layer.empty)
|
||||||
|
const decode = Schema.decode(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
|
||||||
|
TFormValidator extends Validator<TParentData, unknown> | undefined,
|
||||||
|
TData extends I & DeepValue<TParentData, TName>,
|
||||||
|
>(props: {
|
||||||
|
value: TData
|
||||||
|
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
|
||||||
|
signal: AbortSignal
|
||||||
|
}): Promise<ValidationError> =>
|
||||||
|
decode(props.value).pipe(
|
||||||
|
Effect.matchEffect({
|
||||||
|
onSuccess: () => Effect.succeed(null),
|
||||||
|
|
||||||
|
onFailure: e => ArrayFormatter.formatError(e).pipe(
|
||||||
|
Effect.map(flow(
|
||||||
|
Array.map(issue => issue.message),
|
||||||
|
Array.join("\n"),
|
||||||
|
))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
prgm => runtime.runPromise(prgm, { signal: props.signal }),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./Class"
|
export * from "./Class"
|
||||||
export * from "./DateTime"
|
export * from "./DateTime"
|
||||||
|
export * from "./Email"
|
||||||
export * from "./encodedAsPrismaJsonValue"
|
export * from "./encodedAsPrismaJsonValue"
|
||||||
export * from "./Kind"
|
export * from "./Kind"
|
||||||
export * as MobX from "./MobX"
|
export * as MobX from "./MobX"
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ export const toJsonifiable = <
|
|||||||
jsonifiableSchema: Schema.Schema<JsonifiableA, JsonifiableI, JsonifiableR>
|
jsonifiableSchema: Schema.Schema<JsonifiableA, JsonifiableI, JsonifiableR>
|
||||||
) =>
|
) =>
|
||||||
<A, R>(decodedSchema: Schema.Schema<A, JsonifiableA, R>) =>
|
<A, R>(decodedSchema: Schema.Schema<A, JsonifiableA, R>) =>
|
||||||
Schema.compose(jsonifiableSchema, decodedSchema)
|
Schema.compose(jsonifiableSchema, decodedSchema, { strict: true })
|
||||||
|
|||||||
20
src/tests.ts
20
src/tests.ts
@@ -1,5 +1,5 @@
|
|||||||
import { Schema as S } from "@effect/schema"
|
import { Schema as S } from "@effect/schema"
|
||||||
import { reaction, runInAction } from "mobx"
|
import { computed, makeObservable, reaction, runInAction } from "mobx"
|
||||||
import type { Simplify } from "type-fest"
|
import type { Simplify } from "type-fest"
|
||||||
import { MutableTaggedClass, toJsonifiable } from "./Schema"
|
import { MutableTaggedClass, toJsonifiable } from "./Schema"
|
||||||
import { ObservableClass } from "./Schema/MobX"
|
import { ObservableClass } from "./Schema/MobX"
|
||||||
@@ -18,12 +18,24 @@ type TestB = {
|
|||||||
type Merged = Simplify<ExtendAll<[TestA, TestB]>>
|
type Merged = Simplify<ExtendAll<[TestA, TestB]>>
|
||||||
|
|
||||||
|
|
||||||
class User extends MutableTaggedClass<User>()("User", {
|
|
||||||
|
const UserSchema = MutableTaggedClass<User>()("User", {
|
||||||
id: S.BigIntFromSelf,
|
id: S.BigIntFromSelf,
|
||||||
role: S.Union(S.Literal("BasicUser"), S.Literal("Admin")),
|
role: S.Union(S.Literal("BasicUser"), S.Literal("Admin")),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
ObservableClass
|
ObservableClass()
|
||||||
) {}
|
)
|
||||||
|
|
||||||
|
class User extends UserSchema {
|
||||||
|
constructor(...args: ConstructorParameters<typeof UserSchema>) {
|
||||||
|
super(...args)
|
||||||
|
makeObservable(this, { idAsString: computed })
|
||||||
|
}
|
||||||
|
|
||||||
|
get idAsString() {
|
||||||
|
return this.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const JsonifiableUser = User.pipe(
|
const JsonifiableUser = User.pipe(
|
||||||
toJsonifiable(S.Struct({
|
toJsonifiable(S.Struct({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
|||||||
"./src/Layers/index.ts",
|
"./src/Layers/index.ts",
|
||||||
"./src/Schema/index.ts",
|
"./src/Schema/index.ts",
|
||||||
"./src/Schema/MobX/index.ts",
|
"./src/Schema/MobX/index.ts",
|
||||||
|
"./src/Schema/TanStackForm/index.ts",
|
||||||
"./src/Types/index.ts",
|
"./src/Types/index.ts",
|
||||||
],
|
],
|
||||||
format: ["esm", "cjs"],
|
format: ["esm", "cjs"],
|
||||||
|
|||||||
Reference in New Issue
Block a user