@starci/hfs 4.2.13 → 4.2.15
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/CHANGELOG.md +18 -0
- package/README.md +7 -3
- package/emit/db-types.mjs +2 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +200 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +221 -0
- package/runtime/knowledge/hfs/rules.yaml +50 -47
- package/runtime/knowledge/hfs/slots.yaml +307 -179
- package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
- package/runtime/knowledge/patterns/be/projections.yaml +10 -0
- package/runtime/knowledge/patterns/be/queues.yaml +15 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
- package/runtime/knowledge/patterns/be/saga.yaml +13 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
- package/runtime/knowledge/sonar-gate.yaml +10 -2
- package/runtime/modules/kernel/failure-codes.yaml +40 -60
- package/runtime/modules/kernel/removed-vocabulary.yaml +43 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +2 -1
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
- package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
- package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
- package/runtime/scripts/hfs/architecture/index.mjs +2 -0
- package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
- package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
- package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
- package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
- package/runtime/scripts/hfs/check.mjs +10 -6
- package/runtime/scripts/hfs/edition-slots.mjs +13 -4
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
- package/runtime/scripts/hfs/rules/database.mjs +4 -3
- package/runtime/scripts/hfs/rules/edition.mjs +49 -37
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
- package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
- package/scaffold/add-app.mjs +24 -18
- package/scaffold/add-cli-lite.mjs +33 -3
- package/scaffold/add-inbox-lite.mjs +24 -8
- package/scaffold/add-table.mjs +222 -74
- package/scaffold/add.mjs +85 -35
- package/scaffold/app.mjs +140 -36
- package/scaffold/edition-gate.mjs +21 -2
- package/scaffold/lite-exports.mjs +30 -1
- package/scaffold/service.mjs +5 -8
- package/src/main.mjs +2 -2
- package/sync/index.mjs +19 -15
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
- package/templates/app/codegen-graphql.mjs.partial +34 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
- package/templates/app/package-scripts/core.json +1 -0
- package/templates/app/package-scripts-lite/package.json +4 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton/scripts/codegen.mjs +4 -1
- package/templates/app/skeleton-lite/README.md +7 -4
- package/templates/app/skeleton-lite/supabase/config.toml +1 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
- package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
- package/templates/be/inbox/service.spec.ts.tpl +46 -0
- package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
- package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
- package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
- package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
- package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
- package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
- package/templates/be/platform/graphql/graphql.module.ts +49 -0
- package/templates/be/platform/graphql/graphql.options.ts +2 -0
- package/templates/be/platform/graphql/index.ts +1 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
- package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/table/api/create.command.ts.tpl +10 -0
- package/templates/be/table/api/create.contracts.ts.tpl +8 -0
- package/templates/be/table/api/create.handler.ts.tpl +22 -0
- package/templates/be/table/api/create.mapper.ts.tpl +5 -0
- package/templates/be/table/api/create.resolver.ts.tpl +21 -0
- package/templates/be/table/api/create.type.ts.tpl +9 -0
- package/templates/be/table/api/feature.index.ts.tpl +1 -0
- package/templates/be/table/api/feature.module.ts.tpl +7 -0
- package/templates/be/table/api/get.contracts.ts.tpl +11 -0
- package/templates/be/table/api/get.handler.ts.tpl +22 -0
- package/templates/be/table/api/get.input.ts.tpl +12 -0
- package/templates/be/table/api/get.mapper.ts.tpl +9 -0
- package/templates/be/table/api/get.query.ts.tpl +10 -0
- package/templates/be/table/api/get.resolver.ts.tpl +27 -0
- package/templates/be/table/api/get.type.ts.tpl +9 -0
- package/templates/be/table/api/graphql.module.ts.tpl +8 -0
- package/templates/be/table/error.api.ts.tpl +19 -0
- package/templates/be/table/migration.sql.tpl +13 -21
- package/templates/be/table/service.api.ts.tpl +30 -0
- package/templates/be/table/sql.api.ts.tpl +7 -0
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
- package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
- package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
- package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/query.ts.tpl +25 -0
- package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
- package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
- package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
- package/templates/fe/transport/graphql/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
- package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
- package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
- package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
- package/templates/fe/transport/graphql/clients/response.ts +43 -0
- package/upgrade/check.mjs +1 -1
- package/upgrade/index.mjs +3 -2
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
- package/templates/app/quality-config-lite/sonar-project.properties +0 -5
- package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
- package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
- package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
- package/templates/be/patterns/api/http.module.ts.tpl +0 -7
- package/templates/be/patterns/api/http.request.ts.tpl +0 -10
- package/templates/be/patterns/api/http.response.ts.tpl +0 -5
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
- package/templates/fe/table/read.ts.tpl +0 -16
- package/templates/fe/table/write.ts.tpl +0 -16
- /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
- /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
- /package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/component.tsx +0 -0
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use server"
|
|
2
|
-
import "server-only"
|
|
3
|
-
|
|
4
|
-
import { getPrincipal } from "../principal"
|
|
5
|
-
import { createServerDbClient } from "../server"
|
|
6
|
-
import { dbFailure, dbOk, toOutcome } from "../outcome"
|
|
7
|
-
import type { DbOutcome } from "../outcome"
|
|
8
|
-
import { signInInputSchema } from "../validation/validation.mapper"
|
|
9
|
-
|
|
10
|
-
/** Result exposed to the client boundary after sign-in. */
|
|
11
|
-
export interface SignInResult {
|
|
12
|
-
readonly userId: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Schema-checks credentials, signs in through the server client and returns a typed outcome. */
|
|
16
|
-
export const writeSignIn = async (input: FormData): Promise<DbOutcome<SignInResult>> => {
|
|
17
|
-
const principal = await getPrincipal()
|
|
18
|
-
if (principal.kind === "ok") return dbOk({ userId: principal.value.id })
|
|
19
|
-
const parsed = signInInputSchema.safeParse(input)
|
|
20
|
-
if (!parsed.success) return dbFailure("invalid", "sign-in-input")
|
|
21
|
-
const client = await createServerDbClient()
|
|
22
|
-
const signedIn = toOutcome(await client.auth.signInWithPassword(parsed.data))
|
|
23
|
-
if (signedIn.kind !== "ok") return signedIn
|
|
24
|
-
return signedIn.value.user === null ? dbFailure("refused", "user") : dbOk({ userId: signedIn.value.user.id })
|
|
25
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use server"
|
|
2
|
-
import "server-only"
|
|
3
|
-
|
|
4
|
-
import { getPrincipal } from "../principal"
|
|
5
|
-
import { createServerDbClient } from "../server"
|
|
6
|
-
import { dbFailure, dbOk } from "../outcome"
|
|
7
|
-
import type { DbOutcome } from "../outcome"
|
|
8
|
-
|
|
9
|
-
/** Ends the cookie-backed Supabase session, including an already-anonymous one. */
|
|
10
|
-
export const writeSignOut = async (): Promise<DbOutcome<true>> => {
|
|
11
|
-
const principal = await getPrincipal()
|
|
12
|
-
const client = await createServerDbClient()
|
|
13
|
-
const result = await client.auth.signOut()
|
|
14
|
-
if (result.error === null) return dbOk(true)
|
|
15
|
-
if (principal.kind !== "ok" && result.error.status === 401) return dbOk(true)
|
|
16
|
-
return dbFailure(result.error.status === 401 ? "refused" : "unavailable", result.error.code ?? "sign-out")
|
|
17
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { createBrowserClient } from "@supabase/ssr"
|
|
2
|
-
import type { SupabaseClient } from "@supabase/supabase-js"
|
|
3
|
-
import type { Database } from "../../../../../../supabase/types/database.types"
|
|
4
|
-
import { supabaseAnonKey, supabaseUrl } from "@/modules/config"
|
|
5
|
-
|
|
6
|
-
/** Creates the typed public client used only by client-side auth lifecycle hooks. */
|
|
7
|
-
const createBrowserDbClient = (): SupabaseClient<Database> =>
|
|
8
|
-
createBrowserClient<Database>(supabaseUrl(), supabaseAnonKey())
|
|
9
|
-
|
|
10
|
-
/** Owns the browser Auth subscription and exposes only its lifecycle to client hooks. */
|
|
11
|
-
export const subscribeToAuthRefresh = (refresh: () => void): (() => void) => {
|
|
12
|
-
const listener = createBrowserDbClient().auth.onAuthStateChange(refresh)
|
|
13
|
-
return () => listener.data.subscription.unsubscribe()
|
|
14
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/** Failure kinds exposed by the database boundary. */
|
|
2
|
-
export type DbFailureKind = "refused" | "not-found" | "invalid" | "unavailable"
|
|
3
|
-
|
|
4
|
-
/** A successful database or Auth operation. */
|
|
5
|
-
export interface DbOk<T> {
|
|
6
|
-
readonly kind: "ok"
|
|
7
|
-
readonly value: T
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** An expected database or Auth failure. */
|
|
11
|
-
export interface DbFailure {
|
|
12
|
-
readonly kind: DbFailureKind
|
|
13
|
-
readonly code: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The app's one result vocabulary, owned by the lite database transport. */
|
|
17
|
-
export type DbOutcome<T> = DbOk<T> | DbFailure
|
|
18
|
-
|
|
19
|
-
/** The structural part of a Supabase error this boundary interprets. */
|
|
20
|
-
interface SupabaseFailure {
|
|
21
|
-
readonly code?: string
|
|
22
|
-
readonly status?: number
|
|
23
|
-
readonly message: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** The common structural pair returned by Supabase data and Auth calls. */
|
|
27
|
-
export interface SupabaseResult {
|
|
28
|
-
readonly data: unknown
|
|
29
|
-
readonly error: SupabaseFailure | null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The non-null data carried by one concrete SDK result. */
|
|
33
|
-
export type SupabaseData<T extends SupabaseResult> = NonNullable<T["data"]>
|
|
34
|
-
|
|
35
|
-
/** Wraps a value as a successful outcome. */
|
|
36
|
-
export const dbOk = <T>(value: T): DbOk<T> => ({ kind: "ok", value })
|
|
37
|
-
/** Builds a refusal without throwing across a Server Action boundary. */
|
|
38
|
-
export const dbFailure = (kind: DbFailureKind, code: string): DbFailure => ({ kind, code })
|
|
39
|
-
|
|
40
|
-
const hasSignal = (error: SupabaseFailure, signals: ReadonlyArray<string>): boolean => {
|
|
41
|
-
const values = [error.code, error.status === undefined ? undefined : String(error.status)]
|
|
42
|
-
return values.some((value) => value !== undefined && signals.includes(value))
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const hasData = <T>(value: T): value is NonNullable<T> => value !== null && value !== undefined
|
|
46
|
-
|
|
47
|
-
/** Maps Supabase's data/error pair into the app's closed outcome vocabulary. */
|
|
48
|
-
export const toOutcome = <T extends SupabaseResult>(result: T): DbOutcome<SupabaseData<T>> => {
|
|
49
|
-
if (result.error === null) {
|
|
50
|
-
return hasData(result.data) ? dbOk(result.data) : dbFailure("not-found", "empty")
|
|
51
|
-
}
|
|
52
|
-
const code = result.error.code ?? String(result.error.status ?? "transport")
|
|
53
|
-
if (hasSignal(result.error, ["42501", "PGRST301", "401", "403", "invalid_credentials"])) {
|
|
54
|
-
return dbFailure("refused", code)
|
|
55
|
-
}
|
|
56
|
-
if (hasSignal(result.error, ["PGRST116"])) return dbFailure("not-found", code)
|
|
57
|
-
if (result.error.code?.startsWith("23") === true) return dbFailure("invalid", code)
|
|
58
|
-
return dbFailure("unavailable", code)
|
|
59
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import { cache } from "react"
|
|
4
|
-
import { createServerDbClient } from "./server"
|
|
5
|
-
import { dbFailure, dbOk, toOutcome } from "./outcome"
|
|
6
|
-
import type { DbOutcome } from "./outcome"
|
|
7
|
-
|
|
8
|
-
/** The trusted caller claims exposed to server readers and writers. */
|
|
9
|
-
export interface Principal {
|
|
10
|
-
readonly id: string
|
|
11
|
-
readonly email?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Keeps only the claims a consumer may trust after `getClaims()` verified the token. */
|
|
15
|
-
export const principalFromClaims = (claims: Readonly<Record<string, unknown>>): DbOutcome<Principal> => {
|
|
16
|
-
if (typeof claims.sub !== "string") return dbFailure("refused", "claims")
|
|
17
|
-
return dbOk({
|
|
18
|
-
id: claims.sub,
|
|
19
|
-
...(typeof claims.email === "string" ? { email: claims.email } : {}),
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Verifies and memoizes the current request principal. */
|
|
24
|
-
export const getPrincipal = cache(async (): Promise<DbOutcome<Principal>> => {
|
|
25
|
-
const client = await createServerDbClient()
|
|
26
|
-
const claims = toOutcome(await client.auth.getClaims())
|
|
27
|
-
if (claims.kind !== "ok") return claims
|
|
28
|
-
return principalFromClaims(claims.value.claims)
|
|
29
|
-
})
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import { createServerClient } from "@supabase/ssr"
|
|
4
|
-
import type { SupabaseClient } from "@supabase/supabase-js"
|
|
5
|
-
import { cookies } from "next/headers"
|
|
6
|
-
import type { NextRequest, NextResponse } from "next/server"
|
|
7
|
-
import type { Database } from "../../../../../../supabase/types/database.types"
|
|
8
|
-
import { sessionCookieOptions, supabaseAnonKey, supabaseUrl } from "@/modules/config"
|
|
9
|
-
import { toOutcome } from "./outcome"
|
|
10
|
-
import type { DbOutcome, SupabaseData, SupabaseResult } from "./outcome"
|
|
11
|
-
|
|
12
|
-
interface OwnedRowIdentity {
|
|
13
|
-
readonly id: string
|
|
14
|
-
readonly owner_id: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type InsertOwnedRow<T extends SupabaseResult> = (
|
|
18
|
-
client: SupabaseClient<Database>,
|
|
19
|
-
row: OwnedRowIdentity,
|
|
20
|
-
) => PromiseLike<T>
|
|
21
|
-
|
|
22
|
-
/** Creates the cookie-backed server client; the public key keeps every database call under RLS. */
|
|
23
|
-
export const createServerDbClient = async (): Promise<SupabaseClient<Database>> => {
|
|
24
|
-
const store = await cookies()
|
|
25
|
-
return createServerClient<Database>(supabaseUrl(), supabaseAnonKey(), {
|
|
26
|
-
cookies: {
|
|
27
|
-
getAll: () => store.getAll(),
|
|
28
|
-
setAll: (changes) => {
|
|
29
|
-
try {
|
|
30
|
-
for (const change of changes) {
|
|
31
|
-
store.set(change.name, change.value, {
|
|
32
|
-
...change.options,
|
|
33
|
-
...sessionCookieOptions,
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
} catch (cause) {
|
|
37
|
-
if (!(cause instanceof Error) || !cause.message.includes("Cookies can only be modified"))
|
|
38
|
-
throw cause
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Inserts one validated principal-owned row through the typed server client. */
|
|
46
|
-
export const insertRow = async <T extends SupabaseResult>(
|
|
47
|
-
id: string,
|
|
48
|
-
ownerId: string,
|
|
49
|
-
insert: InsertOwnedRow<T>,
|
|
50
|
-
): Promise<DbOutcome<SupabaseData<T>>> => {
|
|
51
|
-
const client = await createServerDbClient()
|
|
52
|
-
return toOutcome(await insert(client, { id, owner_id: ownerId }))
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Creates the proxy client and writes every refreshed cookie onto the response that will be returned. */
|
|
56
|
-
export const createRequestDbClient = (request: NextRequest, response: NextResponse): SupabaseClient<Database> =>
|
|
57
|
-
createServerClient<Database>(supabaseUrl(), supabaseAnonKey(), {
|
|
58
|
-
cookies: {
|
|
59
|
-
getAll: () => request.cookies.getAll(),
|
|
60
|
-
setAll: (changes) => {
|
|
61
|
-
for (const change of changes) {
|
|
62
|
-
request.cookies.set(change.name, change.value)
|
|
63
|
-
response.cookies.set(change.name, change.value, {
|
|
64
|
-
...change.options,
|
|
65
|
-
...sessionCookieOptions,
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
interface OwnedRowInput {
|
|
2
|
-
readonly id: string
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
type OwnedRowParse = { readonly success: true; readonly data: OwnedRowInput } | { readonly success: false }
|
|
6
|
-
|
|
7
|
-
interface SignInInput {
|
|
8
|
-
readonly email: string
|
|
9
|
-
readonly password: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type SignInParse = { readonly success: true; readonly data: SignInInput } | { readonly success: false }
|
|
13
|
-
|
|
14
|
-
const UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u
|
|
15
|
-
const ADDRESS = /^[^\s@]+@([^\s@]+)$/u
|
|
16
|
-
|
|
17
|
-
/** Whether the text is one local part, one `@` and a domain with a dot that has a character on each side; linear in the length. */
|
|
18
|
-
const isEmail = (value: string): boolean => {
|
|
19
|
-
const domain = ADDRESS.exec(value)?.[1]
|
|
20
|
-
if (domain === undefined) return false
|
|
21
|
-
const dot = domain.indexOf(".", 1)
|
|
22
|
-
return dot !== -1 && dot < domain.length - 1
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The bounded UUID schema shared by generated table Server Actions. */
|
|
26
|
-
export const rowSchema = {
|
|
27
|
-
safeParse: (input: unknown): OwnedRowParse => {
|
|
28
|
-
if (
|
|
29
|
-
typeof input !== "object" ||
|
|
30
|
-
input === null ||
|
|
31
|
-
!("id" in input) ||
|
|
32
|
-
typeof input.id !== "string" ||
|
|
33
|
-
!UUID.test(input.id)
|
|
34
|
-
) {
|
|
35
|
-
return { success: false }
|
|
36
|
-
}
|
|
37
|
-
return { success: true, data: { id: input.id } }
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Bounds credentials before a sign-in request reaches Supabase Auth. */
|
|
42
|
-
export const signInInputSchema = {
|
|
43
|
-
safeParse: (input: FormData): SignInParse => {
|
|
44
|
-
const email = input.get("email")
|
|
45
|
-
const password = input.get("password")
|
|
46
|
-
if (
|
|
47
|
-
typeof email !== "string" ||
|
|
48
|
-
email.length > 254 ||
|
|
49
|
-
!isEmail(email) ||
|
|
50
|
-
typeof password !== "string" ||
|
|
51
|
-
password.length < 8 ||
|
|
52
|
-
password.length > 256
|
|
53
|
-
) {
|
|
54
|
-
return { success: false }
|
|
55
|
-
}
|
|
56
|
-
return { success: true, data: { email, password } }
|
|
57
|
-
},
|
|
58
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const INVALID_PATH_CHARACTER = /[\\\u0000-\u001F\u007F]/u
|
|
2
|
-
|
|
3
|
-
const isSingleSlashPath = (value: string): boolean => value.startsWith("/") && !value.startsWith("//")
|
|
4
|
-
|
|
5
|
-
/** Normalizes a same-origin callback destination or falls back to the app root. */
|
|
6
|
-
export const safeNextPath = (candidate: string | null, origin: string): string => {
|
|
7
|
-
if (candidate === null) return "/"
|
|
8
|
-
let destination: URL
|
|
9
|
-
let decoded: string
|
|
10
|
-
try {
|
|
11
|
-
destination = new URL(candidate, origin)
|
|
12
|
-
decoded = decodeURIComponent(candidate)
|
|
13
|
-
} catch {
|
|
14
|
-
return "/"
|
|
15
|
-
}
|
|
16
|
-
if (
|
|
17
|
-
destination.origin !== origin ||
|
|
18
|
-
!isSingleSlashPath(candidate) ||
|
|
19
|
-
!isSingleSlashPath(decoded) ||
|
|
20
|
-
INVALID_PATH_CHARACTER.test(candidate) ||
|
|
21
|
-
INVALID_PATH_CHARACTER.test(decoded)
|
|
22
|
-
) {
|
|
23
|
-
return "/"
|
|
24
|
-
}
|
|
25
|
-
return `${destination.pathname}${destination.search}${destination.hash}`
|
|
26
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import { cache } from "react"
|
|
4
|
-
import type { Database } from "../../../../../../../supabase/types/database.types"
|
|
5
|
-
import { createServerDbClient } from "../server"
|
|
6
|
-
import { toOutcome } from "../outcome"
|
|
7
|
-
import type { DbOutcome } from "../outcome"
|
|
8
|
-
|
|
9
|
-
/** One {{table}} row from the generated database contract. */
|
|
10
|
-
type {{Name}}Row = Database["public"]["Tables"]["{{table}}"]["Row"]
|
|
11
|
-
|
|
12
|
-
/** Reads a bounded page of {{table}} rows visible to the current RLS principal. */
|
|
13
|
-
export const read{{Name}} = cache(async (): Promise<DbOutcome<ReadonlyArray<{{Name}}Row>>> => {
|
|
14
|
-
const client = await createServerDbClient()
|
|
15
|
-
return toOutcome(await client.from("{{table}}").select("*").limit(100))
|
|
16
|
-
})
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use server"
|
|
2
|
-
import "server-only"
|
|
3
|
-
|
|
4
|
-
import { getPrincipal } from "../principal"
|
|
5
|
-
import { dbFailure } from "../outcome"
|
|
6
|
-
import { insertRow } from "../server"
|
|
7
|
-
import { rowSchema } from "../validation/validation.mapper"
|
|
8
|
-
|
|
9
|
-
/** Writes one validated {{table}} row for the current principal. */
|
|
10
|
-
export const write{{Name}} = async (input: unknown) => {
|
|
11
|
-
const principal = await getPrincipal()
|
|
12
|
-
if (principal.kind !== "ok") return dbFailure("refused", "principal")
|
|
13
|
-
const parsed = rowSchema.safeParse(input)
|
|
14
|
-
if (!parsed.success) return dbFailure("invalid", "{{table}}-input")
|
|
15
|
-
return insertRow(parsed.data.id, principal.value.id, (db, row) => db.from("{{table}}").insert(row).select().single())
|
|
16
|
-
}
|
|
File without changes
|
|
File without changes
|