diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..cf1f9ef6 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "extends": [ + "./node_modules/@effect-app/eslint-shared-config/src/oxlintrc.json" + ], + "ignorePatterns": [ + "**/dist/**", + "**/node_modules/**", + "**/*.d.ts", + "**/*.css", + "**/*.scss", + "**/*.vue", + "**/*.json", + "**/*.md", + "**/*.js", + "**/*.jsx", + "**/vitest.config.ts", + "**/eslint.*.mjs", + "**/CHANGELOG.md", + "repos/**" + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8c14bacb..6f2a533f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,9 @@ { "recommendations": [ + "oxc.oxc-vscode", + "dprint.dprint", "dbaeumer.vscode-eslint", "editorconfig.editorconfig", "Vue.volar" ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f966bc5..d2128918 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,40 +26,43 @@ "--max-old-space-size=8192" ], "editor.formatOnSave": true, - "eslint.format.enable": true, + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit", + "source.fixAll.eslint": "explicit", + "source.addMissingImports": "explicit" + }, + "editor.defaultFormatter": "dprint.dprint", "[vue]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": [ - "source.addMissingImports" - ] + "editor.defaultFormatter": "dprint.dprint" }, "[javascript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": [ - "source.addMissingImports" - ] + "editor.defaultFormatter": "dprint.dprint" }, "[javascriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": [ - "source.addMissingImports" - ] + "editor.defaultFormatter": "dprint.dprint" }, "[json]": { - "editor.defaultFormatter": "vscode.json-language-features" + "editor.defaultFormatter": "dprint.dprint" + }, + "[jsonc]": { + "editor.defaultFormatter": "dprint.dprint" }, "[typescript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": [ - "source.addMissingImports" - ] + "editor.defaultFormatter": "dprint.dprint" + }, + "[typescriptvue]": { + "editor.defaultFormatter": "dprint.dprint" }, "[typescriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": [ - "source.addMissingImports" - ] + "editor.defaultFormatter": "dprint.dprint" }, + "eslint.validate": [ + "javascript", + "typescript", + "vue" + ], + "eslint.format.enable": false, "csv-preview.separator": ";", "cSpell.words": [ "codegen", diff --git a/api/eslint.config.mjs b/api/eslint.config.mjs deleted file mode 100644 index 65cd1638..00000000 --- a/api/eslint.config.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import path from "node:path" -import { fileURLToPath } from "node:url" -import { augmentedConfig } from "@effect-app/eslint-shared-config/eslint.base.config" - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -export default [ - ...augmentedConfig(__dirname, false), - { - ignores: [ - "dist/**", - "node_modules/**", - "coverage/**", - "**/*.d.ts", - "**/*.config.ts" - ] - } -] diff --git a/api/nodemon.json b/api/nodemon.json index 71083e32..2f90ecce 100644 --- a/api/nodemon.json +++ b/api/nodemon.json @@ -8,4 +8,4 @@ ".env.local" ], "delay": 333 -} \ No newline at end of file +} diff --git a/api/openapi.json b/api/openapi.json index 83ef031a..bbb79d86 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -462,4 +462,4 @@ "bearerAuth": [] } ] -} \ No newline at end of file +} diff --git a/api/package.json b/api/package.json index 66149730..5a407207 100644 --- a/api/package.json +++ b/api/package.json @@ -15,9 +15,12 @@ "watch": "pnpm build --watch", "watch2": "pnpm clean-dist && pnpm check -w", "check": "NODE_OPTIONS=--max-old-space-size=8192 tsc --build", - "lint": "NODE_OPTIONS=--max-old-space-size=8192 ESLINT_TS=1 eslint src test", - "lint:watch": "ESLINT_TS=1 esw -w --changed --clear --ext ts,tsx src test", - "lint-fix": "pnpm lint --fix", + "lint:oxlint": "oxlint --quiet --type-aware src test", + "lint:oxlint:fix": "oxlint --quiet --type-aware --fix src test", + "format": "dprint fmt --config ../dprint.jsonc .", + "format:check": "dprint check --config ../dprint.jsonc .", + "lint": "pnpm lint:oxlint && pnpm format:check", + "lint-fix": "pnpm lint:oxlint:fix && pnpm format", "test": "vitest", "test:run": "pnpm run test run --passWithNoTests", "testsuite": "pnpm circular && pnpm run test:run && pnpm lint", @@ -141,12 +144,12 @@ "@azure/cosmos": "^4.9.3", "@azure/service-bus": "^7.9.5", "@azure/storage-blob": "^12.31.0", - "@effect-app/infra": "4.0.0-beta.178", - "effect-app": "4.0.0-beta.178", - "@effect/opentelemetry": "4.0.0-beta.59", - "@effect/platform-node": "4.0.0-beta.59", - "@effect/sql-sqlite-node": "4.0.0-beta.59", - "@effect/vitest": "4.0.0-beta.59", + "@effect-app/infra": "4.0.0-beta.218", + "effect-app": "4.0.0-beta.218", + "@effect/opentelemetry": "4.0.0-beta.64", + "@effect/platform-node": "4.0.0-beta.64", + "@effect/sql-sqlite-node": "4.0.0-beta.64", + "@effect/vitest": "4.0.0-beta.64", "@formatjs/cli": "^6.14.4", "@formatjs/intl": "4.1.8", "@mollie/api-client": "^4.5.0", @@ -161,7 +164,7 @@ "cross-fetch": "^4.1.0", "date-fns": "^4.1.0", "dotenv": "^17.4.2", - "effect": "4.0.0-beta.59", + "effect": "4.0.0-beta.64", "express": "^5.2.1", "express-compression": "^1.0.2", "express-oauth2-jwt-bearer": "^1.8.0", @@ -186,8 +189,9 @@ "@types/express": "^5.0.6", "@types/redis": "^2.8.32", "@types/swagger-ui-express": "^4.1.8", - "eslint": "^10.3.0", "dprint": "^0.54.0", + "oxlint": "^1.62.0", + "oxlint-tsgolint": "^0.22.1", "typescript": "~6.0.2" } -} \ No newline at end of file +} diff --git a/api/src/Accounts.controllers.ts b/api/src/Accounts.controllers.ts index c997d7d3..91671658 100644 --- a/api/src/Accounts.controllers.ts +++ b/api/src/Accounts.controllers.ts @@ -12,13 +12,13 @@ export default Router(AccountsRsc)({ *Index() { const users = yield* userRepo.all return users.map((u) => - new UserItem({ + UserItem.make({ id: u.id, name: S.NonEmptyString2k(`${u.name.firstName} ${u.name.lastName}`) }) ) }, - GetMe: userRepo.getCurrentUser + GetMe: () => userRepo.getCurrentUser }) } }) diff --git a/api/src/HelloWorld.controllers.ts b/api/src/HelloWorld.controllers.ts index a3ea4423..1582c5f2 100644 --- a/api/src/HelloWorld.controllers.ts +++ b/api/src/HelloWorld.controllers.ts @@ -32,7 +32,7 @@ export default Router(HelloWorldRsc)({ }) ) - return new GetHelloWorld.success({ + return GetHelloWorld.success.make({ context, echo, state, diff --git a/api/src/Operations.controllers.ts b/api/src/Operations.controllers.ts deleted file mode 100644 index 3449b7f1..00000000 --- a/api/src/Operations.controllers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Router } from "#lib/routing" -import { OperationsRsc } from "#resources" -import { Operations } from "#services" -import { Effect } from "effect-app" -import type { OperationId } from "effect-app/Operations" -import { OperationsDefault } from "./lib/layers.js" - -export default Router(OperationsRsc)({ - dependencies: [OperationsDefault], - *effect(match) { - const operations = yield* Operations - - return match({ - FindOperation: ({ id }: { id: OperationId }) => - operations - .find(id) - .pipe(Effect.map((_) => _.value ?? null)) - }) - } -}) diff --git a/api/src/Users.controllers.ts b/api/src/Users.controllers.ts index a4c1cdb2..85391ab1 100644 --- a/api/src/Users.controllers.ts +++ b/api/src/Users.controllers.ts @@ -1,6 +1,7 @@ import { Router } from "#lib/routing" +import { User } from "#models/User" import { UsersRsc } from "#resources" -import type { UserView } from "#resources/views" +import { UserView } from "#resources/views/UserView" import { Q, UserRepo } from "#services" import { Array } from "effect" import { Effect, Order } from "effect-app" @@ -15,7 +16,10 @@ export default Router(UsersRsc)({ userRepo .query(Q.where("id", "in", req.filterByIds)) .pipe(Effect.map((users) => ({ - users: Array.sort(users, Order.mapInput(Order.String, (_: UserView) => _.displayName)) + users: Array.sort( + users.map((u) => UserView.make({ id: u.id, role: u.role, displayName: User.displayName(u) })), + Order.mapInput(Order.String, (_: UserView) => _.displayName) + ) }))) }) } diff --git a/api/src/_test-decode.js b/api/src/_test-decode.js index e612eb11..cd8ec003 100644 --- a/api/src/_test-decode.js +++ b/api/src/_test-decode.js @@ -1,9 +1,9 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var lib_1 = require("#resources/lib"); -var effect_app_1 = require("effect-app"); -var userProfileFromJson = effect_app_1.S.fromJsonString(lib_1.UserProfile.fromEncoded); +"use strict" +Object.defineProperty(exports, "__esModule", { value: true }) +var lib_1 = require("#resources/lib") +var effect_app_1 = require("effect-app") +var userProfileFromJson = effect_app_1.S.fromJsonString(lib_1.UserProfile.fromEncoded) // Force errors to see what the types resolve to -var _checkDS = ds; -var _checkFEDS = feds; -var _checkUPDS = upds; +var _checkDS = ds +var _checkFEDS = feds +var _checkUPDS = upds diff --git a/api/src/config/base.ts b/api/src/config/base.ts index aea9401c..67a17c4d 100644 --- a/api/src/config/base.ts +++ b/api/src/config/base.ts @@ -16,7 +16,7 @@ export const sendgrid = C.all({ apiKey: C.redacted("sendgridApiKey").pipe(C.withDefault( Redacted.make("") )), - fakeMailAddress: C.string().pipe(C.withDefault("fake-{i}@example.com")), + fakeMailAddress: C.succeed("fake+{i}@example.com"), defaultFrom: C.succeed(FROM), subjectPrefix: env.pipe(C.map((env) => env === "prod" ? "" : `[${serviceName_}] [${env}] `)) }) diff --git a/api/src/controllers.ts b/api/src/controllers.ts index 0d510691..e1037907 100644 --- a/api/src/controllers.ts +++ b/api/src/controllers.ts @@ -2,8 +2,7 @@ import accountsControllers from "./Accounts.controllers.js" import blogControllers from "./Blog.controllers.js" import helloWorldControllers from "./HelloWorld.controllers.js" -import operationsControllers from "./Operations.controllers.js" import usersControllers from "./Users.controllers.js" -export { accountsControllers, blogControllers, helloWorldControllers, operationsControllers, usersControllers } +export { accountsControllers, blogControllers, helloWorldControllers, usersControllers } // codegen:end diff --git a/api/src/lib/layers.ts b/api/src/lib/layers.ts index fbbf55a5..3dff4638 100644 --- a/api/src/lib/layers.ts +++ b/api/src/lib/layers.ts @@ -1,7 +1,5 @@ import { FakeSendgrid } from "@effect-app/infra/Emailer/fake" import { Sendgrid } from "@effect-app/infra/Emailer/Sendgrid" -import { Operations } from "@effect-app/infra/Operations" -import { OperationsRepo } from "@effect-app/infra/OperationsRepo" import { StoreMakerLayer } from "@effect-app/infra/Store/index" import { NodeServices } from "@effect/platform-node" import * as HttpClientNode from "@effect/platform-node/NodeHttpClient" @@ -15,12 +13,12 @@ import { apiConfig, baseConfig } from "../config.js" const ClientLive = SqliteClient .layer({ - filename: "./" + ".data" + "/db.db" + filename: "./.data/db.db" }) .pipe(Layer.provide( Effect .gen(function*() { - const path = "./" + ".data" + const path = "./.data" if (!fs.existsSync(path)) { fs.mkdirSync(path) } @@ -48,10 +46,6 @@ export const EmailerLive = Effect }) .pipe(Layer.unwrap) -export const OperationsDefault = Operations.Live.pipe( - Layer.provide(Layer.effect(OperationsRepo, OperationsRepo.make).pipe(Layer.provide(RepoTest))) -) - export const Platform = HttpClientNode.layerUndici export const ApiPortTag = Context.Service<{ port: number }>("@services/ApiPortTag") diff --git a/api/src/lib/observability.ts b/api/src/lib/observability.ts index 0dfac600..31e282f2 100644 --- a/api/src/lib/observability.ts +++ b/api/src/lib/observability.ts @@ -100,7 +100,7 @@ export class MetricsReader extends Context.Service()("MetricsRead static readonly Live = Layer.effect(this, this.make).pipe(Layer.provide(ExporterRunning.Default)) } -const filteredOps = ["Import.AllOperations", "Operations.FindOperation"] +const filteredOps = ["Import.AllOperations"] const filteredPaths = ["/.well-known/local/server-health", ...filteredOps.map((op) => `/${op}`)] const filteredMethods = ["OPTIONS"] const filterAttrs = { diff --git a/api/src/lib/routing.ts b/api/src/lib/routing.ts index 7a11b454..2e4e6f59 100644 --- a/api/src/lib/routing.ts +++ b/api/src/lib/routing.ts @@ -78,12 +78,8 @@ const RequireRolesLive = Layer.effect( }) ) -class AppMiddlewareImpl extends AppMiddleware { - static Default = this.layer.pipe(Layer.provide([ - AllowAnonymousLive, - RequireRolesLive, - DefaultGenericMiddlewaresLive - ])) -} - -export const { Router, matchAll } = makeRouter(AppMiddlewareImpl) +export const { Router, matchAll } = makeRouter(AppMiddleware.layer.pipe(Layer.provide([ + AllowAnonymousLive, + RequireRolesLive, + DefaultGenericMiddlewaresLive +]))) diff --git a/api/src/models/User.ts b/api/src/models/User.ts index 867ee90e..21ca75db 100644 --- a/api/src/models/User.ts +++ b/api/src/models/User.ts @@ -29,16 +29,16 @@ export const LastName = S export type LastName = typeof LastName.Type -export class FullName extends S.Class("FullName")({ +export class FullName extends S.Opaque()(S.Struct({ firstName: FirstName, lastName: LastName -}) { +})) { static render(this: void, fn: FullName) { return S.NonEmptyString2k(`${fn.firstName} ${fn.lastName}`) } static create(this: void, firstName: FirstName, lastName: LastName) { - return new FullName({ firstName, lastName }) + return FullName.make({ firstName, lastName }) } } @@ -62,15 +62,15 @@ export class UserFromIdResolver extends Context.Service UserFromIdResolver.use((_) => _.get(userId)) } -export class User extends S.Class("User")({ - id: UserId.withDefault, +export class User extends S.Opaque()(S.Struct({ + id: UserId.withConstructorDefault, name: FullName, email: S.Email, role: Role, // passwordHash: S.NonEmptyString255 -}) { - get displayName() { - return S.NonEmptyString2k(this.name.firstName + " " + this.name.lastName) +})) { + static displayName(this: void, u: User) { + return S.NonEmptyString2k(`${u.name.firstName} ${u.name.lastName}`) } static readonly resolver = UserFromIdResolver } diff --git a/api/src/resources.ts b/api/src/resources.ts index f9a464c1..bba44e96 100644 --- a/api/src/resources.ts +++ b/api/src/resources.ts @@ -4,6 +4,5 @@ export { ClientEvents } from "./resources/Events.js" export * as AccountsRsc from "./resources/Accounts.js" export * as BlogRsc from "./resources/Blog.js" export * as HelloWorldRsc from "./resources/HelloWorld.js" -export * as OperationsRsc from "./resources/Operations.js" export * as UsersRsc from "./resources/Users.js" // codegen:end diff --git a/api/src/resources/Events.ts b/api/src/resources/Events.ts index a7ef64ca..31f9cd34 100644 --- a/api/src/resources/Events.ts +++ b/api/src/resources/Events.ts @@ -1,10 +1,10 @@ import { S } from "#resources/lib" import type { Schema } from "effect-app/Schema" -export class BogusEvent extends S.TaggedClass()("BogusEvent", { - id: S.StringId.withDefault, - at: S.Date.withDefault -}) {} +export class BogusEvent extends S.Opaque()(S.TaggedStruct("BogusEvent", { + id: S.StringId.withConstructorDefault, + at: S.Date.withConstructorDefault +})) {} export const ClientEvents = S.Union([BogusEvent]) export type ClientEvents = Schema.Type diff --git a/api/src/resources/HelloWorld.ts b/api/src/resources/HelloWorld.ts index 3dca7172..a49adc6d 100644 --- a/api/src/resources/HelloWorld.ts +++ b/api/src/resources/HelloWorld.ts @@ -7,14 +7,14 @@ import { UserView } from "./views.js" const Req = TaggedRequestFor("HelloWorld") // codegen:end -class Response extends S.Class("Response")({ - now: S.Date.withDefault, +class Response extends S.Opaque()(S.Struct({ + now: S.Date.withConstructorDefault, echo: S.String, state: S.String, context: RequestContext, currentUser: S.NullOr(UserView), randomUser: UserView -}) {} +})) {} export class GetHelloWorld extends Req.Query()("GetHelloWorld", { echo: S.String diff --git a/api/src/resources/Operations.ts b/api/src/resources/Operations.ts deleted file mode 100644 index 8cba626f..00000000 --- a/api/src/resources/Operations.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Duration, Effect } from "effect-app" -import { NotFoundError } from "effect-app/client/errors" -import { Operation, OperationFailure, OperationId } from "effect-app/Operations" -import { clientFor, TaggedRequestFor } from "./lib.js" -import * as S from "./lib/schema.js" - -// codegen:start {preset: meta, sourcePrefix: src/resources/} -const Req = TaggedRequestFor("Operations") -// codegen:end - -export class FindOperation extends Req.Query()("FindOperation", { - id: OperationId -}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(Operation) }) {} - -// Extensions -export const OperationsClient = Effect.gen(function*() { - const opsClient = yield* clientFor({ FindOperation }) - - function refreshAndWaitAForOperation( - refresh: Effect.Effect, - cb?: (op: Operation) => void - ) { - return (act: Effect.Effect) => - Effect.tap( - waitForOperation( - Effect.tap(act, refresh), - cb - ), - refresh - ) - } - - function refreshAndWaitAForOperation_( - act: Effect.Effect, - refresh: Effect.Effect, - cb?: (op: Operation) => void - ) { - return Effect.tap( - waitForOperation( - Effect.tap(act, refresh), - cb - ), - refresh - ) - } - - function refreshAndWaitForOperation(refresh: Effect.Effect, cb?: (op: Operation) => void) { - return (act: (req: Req) => Effect.Effect) => (req: Req) => - refreshAndWaitAForOperation_(act(req), refresh, cb) - } - - function refreshAndWaitForOperation_( - act: (req: Req) => Effect.Effect, - refresh: Effect.Effect, - cb?: (op: Operation) => void - ) { - return (req: Req) => refreshAndWaitAForOperation_(act(req), refresh, cb) - } - - function waitForOperation( - self: Effect.Effect, - cb?: (op: Operation) => void - ) { - return Effect.flatMap(self, (r) => _waitForOperation(r, cb)) - } - - function waitForOperation_(cb?: (op: Operation) => void) { - return (self: (req: Req) => Effect.Effect) => (req: Req) => - Effect.flatMap(self(req), (r) => _waitForOperation(r, cb)) - } - - const isFailure = S.is(OperationFailure) - - function _waitForOperation(id: OperationId, cb?: (op: Operation) => void) { - return Effect - .gen(function*() { - let r = yield* opsClient.FindOperation.handler({ id }) - while (r) { - if (cb) cb(r) - const result = r.result - if (result) return isFailure(result) ? yield* Effect.fail(result) : yield* Effect.succeed(result) - yield* Effect.sleep(Duration.seconds(2)) - r = yield* opsClient.FindOperation.handler({ id }) - } - return yield* new NotFoundError({ type: "Operation", id }) - }) - // .pipe(Effect.provide(Layer.setRequestCaching(false))) - } - - return Object.assign(opsClient, { - refreshAndWaitAForOperation, - refreshAndWaitAForOperation_, - refreshAndWaitForOperation, - refreshAndWaitForOperation_, - waitForOperation, - waitForOperation_ - }) -}) diff --git a/api/src/resources/lib/Userprofile.ts b/api/src/resources/lib/Userprofile.ts index 35d164d7..f518ac17 100644 --- a/api/src/resources/lib/Userprofile.ts +++ b/api/src/resources/lib/Userprofile.ts @@ -4,10 +4,12 @@ import { UserProfileId } from "effect-app/ids" // TODO: move back to services, and remove reference need in resources or frontend export class UserProfile extends Context.assignTag("UserProfile")( - S.Class("UserProfile")({ - sub: UserProfileId, - roles: S.Array(Role).withDefault - }) -) { - static readonly Codec = S.revealCodec(S.encodeKeys({ roles: "https://nomizz.com/roles" })(this)) -} + S.Opaque()( + S + .Struct({ + sub: UserProfileId, + roles: S.Array(Role).withConstructorDefault + }) + .pipe(S.encodeKeys({ roles: "https://nomizz.com/roles" })) + ) +) {} diff --git a/api/src/resources/lib/req.ts b/api/src/resources/lib/req.ts index b5252652..dddcdccc 100644 --- a/api/src/resources/lib/req.ts +++ b/api/src/resources/lib/req.ts @@ -1,9 +1,9 @@ import { Layer } from "effect-app" import { makeRpcClient } from "effect-app/client" import { ApiClientFactory } from "effect-app/client/apiClientFactory" -import { RequestContextMap } from "./middleware.js" +import { AppMiddleware } from "./middleware.js" -export const { TaggedRequestFor } = makeRpcClient(RequestContextMap) +export const { TaggedRequestFor } = makeRpcClient(AppMiddleware) export const RequestCacheLayers = Layer.empty export const clientFor = ApiClientFactory.makeFor(RequestCacheLayers) diff --git a/api/src/resources/views/UserItem.ts b/api/src/resources/views/UserItem.ts index 6da0952a..88f9ddf6 100644 --- a/api/src/resources/views/UserItem.ts +++ b/api/src/resources/views/UserItem.ts @@ -1,10 +1,10 @@ import { User } from "#models/User" import { S } from "#resources/lib" -export class UserItem extends S.Class("UserItem")({ +export class UserItem extends S.Opaque()(S.Struct({ id: User.fields.id, name: S.NonEmptyString2k -}) {} +})) {} // codegen:start {preset: model} // diff --git a/api/src/services/DBContext/UserRepo.ts b/api/src/services/DBContext/UserRepo.ts index c3fcc0e9..a2c728a8 100644 --- a/api/src/services/DBContext/UserRepo.ts +++ b/api/src/services/DBContext/UserRepo.ts @@ -5,9 +5,12 @@ import { Model } from "@effect-app/infra" import { NotFoundError, NotLoggedInError } from "@effect-app/infra/errors" import { generate } from "@effect-app/infra/test" import { Array, Context, Effect, Exit, Layer, Option, pipe, Request, RequestResolver, S } from "effect-app" +import { fakerArb } from "effect-app/faker" +import { UserProfileId } from "effect-app/ids" +import { Email } from "effect-app/Schema" +import fc from "fast-check" import { Q } from "../lib.js" import { UserProfile } from "../UserProfile.js" -import { StringId } from "effect-app/Schema" export interface UserPersistenceModel extends S.Codec.Encoded { _etag: string | undefined @@ -26,16 +29,18 @@ export class UserRepo extends Context.Service()("UserRepo", { .range(1, 8) .map((_, i): User => { const g = generate(S.toArbitrary(User)).value - // const emailArb = fakerArb((_) => () => - // _ - // .internet - // .exampleEmail({ firstName: g.name.firstName, lastName: g.name.lastName }) - // ) - return new User({ + const emailArb = fakerArb((_) => () => + _ + .internet + .exampleEmail({ firstName: g.name.firstName, lastName: g.name.lastName }) + ) + const role = i === 0 || i === 1 ? "manager" : "user" + return User.make({ ...g, - id: StringId.make(), - //email: Email(generate(emailArb(fc)).value), - role: i === 0 || i === 1 ? "manager" : "user" + id: UserProfileId("fake-user-" + i), + name: { firstName: S.NonEmptyString255(`Fake${i}First`), lastName: S.NonEmptyString255(`Fake${i}Last`) }, + email: Email(generate(emailArb(fc)).value), + role }) }), Array.toNonEmptyArray, diff --git a/api/src/services/UserProfile.ts b/api/src/services/UserProfile.ts index 081aa666..1fec6058 100644 --- a/api/src/services/UserProfile.ts +++ b/api/src/services/UserProfile.ts @@ -12,8 +12,8 @@ export namespace UserProfileService { } } -const userProfileFromJson = S.fromJsonString(UserProfile.Codec) -const userProfileFromJWT = parseJwt(UserProfile.Codec) +const userProfileFromJson = S.fromJsonString(S.toCodecJson(UserProfile)) +const userProfileFromJWT = parseJwt(UserProfile) // Workaround: Schema.encodeKeys has a TypeScript inference limitation where it cannot resolve // DecodingServices through its complex mapped type, falling back to `unknown` from the `Top` constraint. diff --git a/api/src/services/lib.ts b/api/src/services/lib.ts index 651a6299..a7a922f9 100644 --- a/api/src/services/lib.ts +++ b/api/src/services/lib.ts @@ -1,7 +1,6 @@ export * from "@effect-app/infra/adapters/memQueue" export * from "@effect-app/infra/adapters/ServiceBus" export * from "@effect-app/infra/Emailer" -export * from "@effect-app/infra/Operations" export * from "@effect-app/infra/Store/index" export { Q } from "@effect-app/infra/Model" diff --git a/api/test/setup.ts b/api/test/setup.ts index e8a91081..f802fc47 100644 --- a/api/test/setup.ts +++ b/api/test/setup.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-unassigned-import */ import "#lib/basicRuntime" import "./setup2.js" diff --git a/api/tsconfig.json b/api/tsconfig.json index 6b44d54a..5ca311fe 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -25,7 +25,7 @@ "#services": [ "./src/services.js" ] - }, + } }, "extends": "../tsconfig.base.json", "references": [ @@ -35,5 +35,5 @@ { "path": "./tsconfig.test.json" } - ], -} \ No newline at end of file + ] +} diff --git a/api/tsconfig.src.json b/api/tsconfig.src.json index f3071ecf..b43b7b0b 100644 --- a/api/tsconfig.src.json +++ b/api/tsconfig.src.json @@ -39,7 +39,7 @@ "../types/modules", "vite/types/importMeta.d.ts" ], - "outDir": "./dist", + "outDir": "./dist" }, "include": [ "./src/**/*.ts" @@ -57,4 +57,4 @@ "**/.*", "**/*.tmp" ] -} \ No newline at end of file +} diff --git a/api/tsconfig.test.json b/api/tsconfig.test.json index aa2417a4..a9b745de 100644 --- a/api/tsconfig.test.json +++ b/api/tsconfig.test.json @@ -53,6 +53,6 @@ "references": [ { "path": "./tsconfig.src.json" - }, + } ] -} \ No newline at end of file +} diff --git a/api/tsconfig.test.local.json b/api/tsconfig.test.local.json index ab802170..285f98e3 100644 --- a/api/tsconfig.test.local.json +++ b/api/tsconfig.test.local.json @@ -4,10 +4,10 @@ "rootDir": ".", "outDir": "./test/dist", "tsBuildInfoFile": "./test/dist/.local.tsbuildinfo", - "noEmit": true, + "noEmit": true }, "include": [ "./test/**/*.ts", "./src/**/*.ts" ] -} \ No newline at end of file +} diff --git a/dprint.jsonc b/dprint.jsonc new file mode 100644 index 00000000..0fb4c461 --- /dev/null +++ b/dprint.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "https://dprint.dev/schemas/v0.json", + "extends": ["./node_modules/@effect-app/eslint-shared-config/src/dprint.jsonc"], + // excludes overwrites (not merges) the base — all standard patterns must be repeated here. + "excludes": [ + "**/node_modules", + "**/*-lock.json", + "**/dist/**", + "**/.nuxt/**", + "**/.output/**", + "**/coverage/**", + "**/auto-imports.d.ts", + "repos/**", + "patches/**", + "**/*.wsdl", + "**/*.xsd", + ], +} diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs deleted file mode 100644 index 73acf230..00000000 --- a/e2e/eslint.config.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { augmentedConfig } from "@effect-app/eslint-shared-config/eslint.base.config" - -import path from "node:path" -import { fileURLToPath } from "node:url" - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -export default [ - ...augmentedConfig(__dirname, false, "tsconfig.all2.json"), - { - ignores: [ - "eslint.config.mjs" - ] - } -] diff --git a/e2e/package.json b/e2e/package.json index bef5ad41..488bdc61 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -8,9 +8,12 @@ "test": "playwright test", "test:watch": "playwright-watch test", "build": "tsc --build", - "lint": "NODE_OPTIONS=--max-old-space-size=8192 ESLINT_TS=1 eslint .", - "lint:watch": "ESLINT_TS=1 esw -w --changed --clear --ext ts,tsx .", - "lint-fix": "pnpm lint --fix", + "lint:oxlint": "oxlint --quiet --type-aware .", + "lint:oxlint:fix": "oxlint --quiet --type-aware --fix .", + "format": "dprint fmt --config ../dprint.jsonc .", + "format:check": "dprint check --config ../dprint.jsonc .", + "lint": "pnpm lint:oxlint && pnpm format:check", + "lint-fix": "pnpm lint:oxlint:fix && pnpm format", "up": "pnpm run update && pnpm exec-update", "update": "pnpm ncu -u", "exec-update": "pnpm i", @@ -23,6 +26,8 @@ "@types/node": "~25.6.0", "date-fns": "^4.1.0", "npm-check-updates": "^22.1.0", + "oxlint": "^1.62.0", + "oxlint-tsgolint": "^0.22.1", "playwright-core": "^1.59.1", "playwright-watch": "^1.3.23", "prettier": "^3.8.3", @@ -30,11 +35,10 @@ }, "dependencies": { "@effect-app-boilerplate/api": "workspace:*", - "@effect/platform-node": "4.0.0-beta.59", - "effect-app": "4.0.0-beta.178", - "effect": "4.0.0-beta.59", + "@effect/platform-node": "4.0.0-beta.64", + "effect-app": "4.0.0-beta.218", + "effect": "4.0.0-beta.64", "cross-fetch": "^4.1.0", - "eslint": "^10.3.0", "dprint": "^0.54.0" } -} \ No newline at end of file +} diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 402ca11e..d00b453f 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "baseUrl": ".", + "rootDir": ".", "outDir": "./test-out", "lib": [ "es5", @@ -13,7 +13,7 @@ ] }, // helps performance - "disableSourceOfProjectReferenceRedirect": true, + "disableSourceOfProjectReferenceRedirect": true // "transformers": [ // // Transform paths in output .js files // { @@ -38,4 +38,4 @@ "path": "../api" } ] -} \ No newline at end of file +} diff --git a/frontend/composables/client.ts b/frontend/composables/client.ts index 6a186526..4d16af3b 100644 --- a/frontend/composables/client.ts +++ b/frontend/composables/client.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { clientFor as clientFor_ } from "#resources/lib" -import { OperationsClient } from "#resources/Operations" +import { UserViews } from "#resources/resolvers/UserResolver" +import type { makeIntl } from "@effect-app/vue" import { Commander } from "@effect-app/vue/commander" import { Confirm } from "@effect-app/vue/confirm" import { I18n } from "@effect-app/vue/intl" @@ -12,8 +13,6 @@ import { Effect, Layer, ManagedRuntime } from "effect-app" import { useToast } from "vue-toastification" import type { RT } from "~/plugins/runtime" import { useIntl } from "./intl" -import { UserViews } from "#resources/resolvers/UserResolver" -import type { makeIntl } from "@effect-app/vue" export { useToast } from "vue-toastification" @@ -54,8 +53,8 @@ const commanderLayer = Commander.Default.pipe( ) const globalLayers = Layer.effect(UserViews, UserViews.make()).pipe( - Layer.provideMerge(Effect.sync(() => useRuntime().globalLayers).pipe(Layer.unwrap -))) + Layer.provideMerge(Effect.sync(() => useRuntime().globalLayers).pipe(Layer.unwrap)) +) const viewLayers = Layer.mergeAll(Router.Default, intlLayer, toastLayer) const provideLayers = Layer .mergeAll( @@ -72,5 +71,3 @@ export const { Command, clientFor } = makeClient( clientFor_, Router.Default ) - -export const useOperationsClient = () => useRuntime().runSync(OperationsClient) diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 1f440760..56291014 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -2,6 +2,7 @@ // @ts-nocheck import { vueConfig } from "@effect-app/eslint-shared-config/eslint.vue.config" +import vuetify from "eslint-plugin-vuetify" import path from "node:path" import { fileURLToPath } from "node:url" @@ -11,18 +12,20 @@ const __dirname = path.dirname(__filename) export default [ ...vueConfig(__dirname, false), + // Vuetify 4 upgrade rules: typography MD2→MD3 renames, elevation overflow, legacy grid props, deprecated snackbar + ...vuetify.configs["flat/recommended-v4"], { - ignores: [".nuxt/**", ".output/**"], + ignores: [".nuxt/**", ".output/**", "**/*.ts"] }, { files: ["pages/**/*.vue", "components/**/*.vue", "layouts/**/*.vue"], rules: { - "vue/multi-word-component-names": "off", - }, + "vue/multi-word-component-names": "off" + } }, { rules: { - "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-explicit-any": "warn" } } ] diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index adf66946..65e07d7b 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -1,4 +1,5 @@