@starci/hfs 4.2.12 → 4.2.14
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 +26 -1
- package/README.md +7 -3
- package/emit/db-types.mjs +2 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
- package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
- package/runtime/knowledge/hfs/rules.yaml +51 -48
- package/runtime/knowledge/hfs/slots.yaml +319 -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 +48 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +3 -2
- 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 +146 -38
- 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 +27 -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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client"
|
|
2
|
+
import {
|
|
3
|
+
createAttachLocaleLink,
|
|
4
|
+
createErrorLink,
|
|
5
|
+
createHttpLink,
|
|
6
|
+
createRetryLink,
|
|
7
|
+
createTimeoutLink,
|
|
8
|
+
defaultOptions,
|
|
9
|
+
} from "../links"
|
|
10
|
+
import type { ErrorReporter } from "../links/error"
|
|
11
|
+
|
|
12
|
+
/** What the anonymous client takes: the abort signal of its operations and where the error link reports (none: it stays silent). */
|
|
13
|
+
export interface CreateNoAuthApolloClientOptions {
|
|
14
|
+
readonly signal?: AbortSignal
|
|
15
|
+
readonly report?: ErrorReporter
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The anonymous Apollo client for public operations: retry -> error -> timeout -> locale -> http. It never attaches a
|
|
20
|
+
* token, so a public operation stays public even for a signed-in reader.
|
|
21
|
+
*/
|
|
22
|
+
export const createNoAuthApolloClient = ({ signal, report }: CreateNoAuthApolloClientOptions = {}) =>
|
|
23
|
+
new ApolloClient({
|
|
24
|
+
link: ApolloLink.from([
|
|
25
|
+
createRetryLink(),
|
|
26
|
+
createErrorLink(report),
|
|
27
|
+
createTimeoutLink(),
|
|
28
|
+
createAttachLocaleLink(),
|
|
29
|
+
createHttpLink({ signal }),
|
|
30
|
+
]),
|
|
31
|
+
cache: new InMemoryCache(),
|
|
32
|
+
defaultOptions,
|
|
33
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SetContextLink } from "@apollo/client/link/context"
|
|
2
|
+
|
|
3
|
+
/** Attaches `Authorization: Bearer <access token>` from modules/auth when a session exists; an anonymous call goes out without it. */
|
|
4
|
+
export const createAttachAccessTokenLink = (accessToken: () => Promise<string | undefined>) =>
|
|
5
|
+
new SetContextLink(async (context) => {
|
|
6
|
+
const token = await accessToken()
|
|
7
|
+
if (token === undefined) return {}
|
|
8
|
+
return { headers: { ...context.headers, authorization: `Bearer ${token}` } }
|
|
9
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors, CombinedProtocolErrors } from "@apollo/client"
|
|
2
|
+
import { ErrorLink } from "@apollo/client/link/error"
|
|
3
|
+
|
|
4
|
+
/** Where the error link reports one failed operation, as one line (the app's observability, when it wires one). */
|
|
5
|
+
export type ErrorReporter = (line: string) => void
|
|
6
|
+
|
|
7
|
+
/** One GraphQL error as the combined error carries it: its message and its extensions. */
|
|
8
|
+
type GraphQLErrorEntry = { readonly message: string; readonly extensions?: Readonly<Record<string, unknown>> }
|
|
9
|
+
|
|
10
|
+
/** The code of one GraphQL error (`extensions.code`), else its message. */
|
|
11
|
+
const codeOf = (error: GraphQLErrorEntry): string =>
|
|
12
|
+
typeof error.extensions?.code === "string" ? error.extensions.code : error.message
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reports what came back from downstream to `report` when the app passes one. The operation maps every error to its
|
|
16
|
+
* GraphQLResponse, so this link never swallows or rewrites one.
|
|
17
|
+
*/
|
|
18
|
+
export const createErrorLink = (report?: ErrorReporter) =>
|
|
19
|
+
new ErrorLink(({ error, operation }) => {
|
|
20
|
+
if (report === undefined) return
|
|
21
|
+
if (CombinedGraphQLErrors.is(error)) {
|
|
22
|
+
report(`[graphql] ${operation.operationName}: ${error.errors.map(codeOf).join(", ")}`)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if (CombinedProtocolErrors.is(error)) {
|
|
26
|
+
report(`[graphql protocol] ${operation.operationName}: ${error.message}`)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
report(`[graphql network] ${operation.operationName}: ${error.message}`)
|
|
30
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HttpLink } from "@apollo/client"
|
|
2
|
+
import { graphqlUrl } from "@/modules/config"
|
|
3
|
+
|
|
4
|
+
/** What the terminal link takes: whether cookies travel and the abort signal of every operation of the client. */
|
|
5
|
+
export interface CreateHttpLinkParams {
|
|
6
|
+
readonly withCredentials?: boolean
|
|
7
|
+
readonly signal?: AbortSignal
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The terminal link: POSTs the operation to the back end's GraphQL endpoint (modules/config). */
|
|
11
|
+
export const createHttpLink = ({ withCredentials = false, signal }: CreateHttpLinkParams = {}) =>
|
|
12
|
+
new HttpLink({
|
|
13
|
+
uri: graphqlUrl(),
|
|
14
|
+
credentials: withCredentials ? "include" : "same-origin",
|
|
15
|
+
fetchOptions: { signal },
|
|
16
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createAttachAccessTokenLink } from "./attach-access-token"
|
|
2
|
+
export { createErrorLink } from "./error"
|
|
3
|
+
export { createHttpLink } from "./http"
|
|
4
|
+
export { createAttachLocaleLink } from "./locale"
|
|
5
|
+
export { defaultOptions } from "./options"
|
|
6
|
+
export { createRefreshTokenLink } from "./refresh-token"
|
|
7
|
+
export { createRetryLink } from "./retry"
|
|
8
|
+
export { createTimeoutLink } from "./timeout"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SetContextLink } from "@apollo/client/link/context"
|
|
2
|
+
|
|
3
|
+
/** The language of the document the reader is on (`<html lang>`, set by the locale layout); undefined outside a browser. */
|
|
4
|
+
const documentLocale = (): string | undefined =>
|
|
5
|
+
typeof document === "undefined" || document.documentElement.lang === "" ? undefined : document.documentElement.lang
|
|
6
|
+
|
|
7
|
+
/** The one writer of `x-locale`: every operation declares the language the back end answers and localizes its errors in. */
|
|
8
|
+
export const createAttachLocaleLink = (locale: () => string | undefined = documentLocale) =>
|
|
9
|
+
new SetContextLink((context) => {
|
|
10
|
+
const value = locale()
|
|
11
|
+
if (value === undefined) return {}
|
|
12
|
+
return { headers: { ...context.headers, "x-locale": value } }
|
|
13
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApolloClient } from "@apollo/client"
|
|
2
|
+
|
|
3
|
+
/** How long one operation may run before the timeout link fails it. */
|
|
4
|
+
export const OPERATION_TIMEOUT_MS = 15_000
|
|
5
|
+
|
|
6
|
+
/** The retry window of a transient network failure: the first delay, the longest delay and the attempts in all. */
|
|
7
|
+
export const RETRY_POLICY = { initialDelayMs: 300, maxDelayMs: 3_000, maxAttempts: 3 } as const
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The client policies: SWR owns the cache, so Apollo never answers from its own, and a GraphQL error comes back as the
|
|
11
|
+
* result's `error` (the operation maps it to a GraphQLResponse) instead of a rejected promise.
|
|
12
|
+
*/
|
|
13
|
+
export const defaultOptions: ApolloClient.DefaultOptions = {
|
|
14
|
+
watchQuery: { fetchPolicy: "no-cache", errorPolicy: "all" },
|
|
15
|
+
query: { fetchPolicy: "no-cache", errorPolicy: "all" },
|
|
16
|
+
mutate: { errorPolicy: "all" },
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors, Observable } from "@apollo/client"
|
|
2
|
+
import { ErrorLink } from "@apollo/client/link/error"
|
|
3
|
+
|
|
4
|
+
/** The context flag of an operation already replayed after a refresh: it is never refreshed twice. */
|
|
5
|
+
const REFRESHED = "accessTokenRefreshed"
|
|
6
|
+
|
|
7
|
+
/** True when the back end refused the operation because the caller is not signed in (kind `unauthenticated`). */
|
|
8
|
+
const isUnauthenticated = (error: unknown): boolean =>
|
|
9
|
+
CombinedGraphQLErrors.is(error) &&
|
|
10
|
+
error.errors.some(
|
|
11
|
+
(each) => each.extensions?.kind === "unauthenticated" || each.extensions?.code === "UNAUTHENTICATED",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* On an UNAUTHENTICATED answer, asks modules/auth for a fresh access token once and replays the operation with it. When no
|
|
16
|
+
* token comes back (the session ended) the original error travels on.
|
|
17
|
+
*/
|
|
18
|
+
export const createRefreshTokenLink = (refreshAccessToken: () => Promise<string | undefined>) =>
|
|
19
|
+
new ErrorLink(({ error, operation, forward }) => {
|
|
20
|
+
if (!isUnauthenticated(error) || operation.getContext()[REFRESHED] === true) return
|
|
21
|
+
return new Observable((observer) => {
|
|
22
|
+
let subscription: { unsubscribe: () => void } | undefined
|
|
23
|
+
refreshAccessToken().then(
|
|
24
|
+
(token) => {
|
|
25
|
+
if (token === undefined) {
|
|
26
|
+
observer.error(error)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
operation.setContext({ [REFRESHED]: true })
|
|
30
|
+
subscription = forward(operation).subscribe(observer)
|
|
31
|
+
},
|
|
32
|
+
(cause: unknown) => observer.error(cause),
|
|
33
|
+
)
|
|
34
|
+
return () => subscription?.unsubscribe()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors } from "@apollo/client"
|
|
2
|
+
import { RetryLink } from "@apollo/client/link/retry"
|
|
3
|
+
import { RETRY_POLICY } from "./options"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retries a transient network failure with jittered exponential backoff. A GraphQL error is an answer of the back end and
|
|
7
|
+
* is never retried. First in the chain, so every other link sees the operation once per attempt.
|
|
8
|
+
*/
|
|
9
|
+
export const createRetryLink = () =>
|
|
10
|
+
new RetryLink({
|
|
11
|
+
delay: { initial: RETRY_POLICY.initialDelayMs, max: RETRY_POLICY.maxDelayMs, jitter: true },
|
|
12
|
+
attempts: {
|
|
13
|
+
max: RETRY_POLICY.maxAttempts,
|
|
14
|
+
retryIf: (error) => error !== undefined && !CombinedGraphQLErrors.is(error),
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ApolloLink, Observable } from "@apollo/client"
|
|
2
|
+
import { OPERATION_TIMEOUT_MS } from "./options"
|
|
3
|
+
|
|
4
|
+
/** Fails the operation when no answer arrived within the timeout; the upstream links see the timeout as an error. */
|
|
5
|
+
export const createTimeoutLink = (timeoutMs = OPERATION_TIMEOUT_MS) =>
|
|
6
|
+
new ApolloLink(
|
|
7
|
+
(operation, forward) =>
|
|
8
|
+
new Observable((observer) => {
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
observer.error(new Error(`${operation.operationName} timed out after ${timeoutMs}ms`))
|
|
11
|
+
}, timeoutMs)
|
|
12
|
+
const subscription = forward(operation).subscribe({
|
|
13
|
+
next: (value) => observer.next(value),
|
|
14
|
+
error: (cause: unknown) => {
|
|
15
|
+
clearTimeout(timer)
|
|
16
|
+
observer.error(cause)
|
|
17
|
+
},
|
|
18
|
+
complete: () => {
|
|
19
|
+
clearTimeout(timer)
|
|
20
|
+
observer.complete()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
return () => {
|
|
24
|
+
clearTimeout(timer)
|
|
25
|
+
subscription.unsubscribe()
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors } from "@apollo/client"
|
|
2
|
+
import type { GraphQLFailure, GraphQLResponse } from "../types"
|
|
3
|
+
|
|
4
|
+
/** The code of a failure the back end never answered: network, timeout or abort. */
|
|
5
|
+
const TRANSPORT_FAILED = "TRANSPORT_FAILED"
|
|
6
|
+
|
|
7
|
+
/** What an Apollo query or mutation resolves to under the `all` error policy: the data it got and the error it met. */
|
|
8
|
+
export interface ApolloOutcome<TData> {
|
|
9
|
+
readonly data?: TData | null
|
|
10
|
+
readonly error?: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const transportFailure = (message: string): GraphQLFailure => ({
|
|
14
|
+
success: false,
|
|
15
|
+
error: TRANSPORT_FAILED,
|
|
16
|
+
kind: "unavailable",
|
|
17
|
+
message,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/** The failure of an Apollo error: the first GraphQL error's code, kind and (localized) message, else a transport failure. */
|
|
21
|
+
const failureOf = (error: unknown): GraphQLFailure => {
|
|
22
|
+
if (!CombinedGraphQLErrors.is(error))
|
|
23
|
+
return transportFailure(error instanceof Error ? error.message : TRANSPORT_FAILED)
|
|
24
|
+
const first = error.errors[0]
|
|
25
|
+
const code = first?.extensions?.code
|
|
26
|
+
const kind = first?.extensions?.kind
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: typeof code === "string" ? code : TRANSPORT_FAILED,
|
|
30
|
+
kind: typeof kind === "string" ? kind : "internal",
|
|
31
|
+
message: first?.message ?? TRANSPORT_FAILED,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Maps one Apollo outcome to the GraphQLResponse of the operation, `pick` selecting the operation's field from the data. */
|
|
36
|
+
export const graphqlResponseOf = <TData, TValue>(
|
|
37
|
+
outcome: ApolloOutcome<TData>,
|
|
38
|
+
pick: (data: TData) => TValue,
|
|
39
|
+
): GraphQLResponse<TValue> => {
|
|
40
|
+
if (outcome.error !== undefined) return failureOf(outcome.error)
|
|
41
|
+
if (outcome.data === undefined || outcome.data === null) return transportFailure(TRANSPORT_FAILED)
|
|
42
|
+
return { success: true, data: pick(outcome.data) }
|
|
43
|
+
}
|
package/upgrade/check.mjs
CHANGED
|
@@ -88,7 +88,7 @@ export async function checkEdition({
|
|
|
88
88
|
|
|
89
89
|
const full = fullEditionDeclaration(declaration, manifest);
|
|
90
90
|
const tracked = trackedFiles(repoRoot);
|
|
91
|
-
// A lite app
|
|
91
|
+
// A lite app scaffolded before the 2026-10-12 ruling has no Jest preset installed. The upgrade model carries the preset's public sync inputs
|
|
92
92
|
// specifically so both the dry full-edition view and the upgrade plan render the same targets before dependencies are added.
|
|
93
93
|
const resolvedPresets = presets ?? UPGRADE_PRESETS;
|
|
94
94
|
const managed = renderTargets(full, resolvedPresets, { manifest });
|
package/upgrade/index.mjs
CHANGED
|
@@ -42,7 +42,8 @@ const FULL_DEV_DEPENDENCIES = Object.freeze([
|
|
|
42
42
|
"ts-jest",
|
|
43
43
|
]);
|
|
44
44
|
const FULL_DEPENDENCIES = Object.freeze(["nest-commander"]);
|
|
45
|
-
// Upgrade must render the full managed files before it can add/install the
|
|
45
|
+
// Upgrade must render the full managed files before it can add/install the Jest preset a lite app scaffolded before the 2026-10-12
|
|
46
|
+
// ruling lacks (a current lite app already has it, and the e2e world's @starci/test-world is what full adds). These are the preset's two
|
|
46
47
|
// public sync inputs; once apply resolves the new dependency, normal full sync reads them from @starci/jest-preset again.
|
|
47
48
|
export const UPGRADE_PRESETS = Object.freeze({
|
|
48
49
|
sonarExclusions: "**/*.spec.ts,**/*.e2e-spec.ts,**/dist/**,**/coverage/**",
|
|
@@ -349,7 +350,7 @@ async function model({ root, to, presets, manifest }) {
|
|
|
349
350
|
steps.push({
|
|
350
351
|
op: "rewrite-managed",
|
|
351
352
|
path: "package.json",
|
|
352
|
-
why: "Add the full test toolchain at canon pins and render the full root scripts.",
|
|
353
|
+
why: "Add the full test toolchain (the e2e world and any unit tooling a lite app lacks) at canon pins and render the full root scripts.",
|
|
353
354
|
content: jsonText(nextPackage),
|
|
354
355
|
kind: "file",
|
|
355
356
|
});
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { chainParts, isSupabaseCall, isSupabaseValue, reportAt, unwrap } from './supabase-ast.mjs';
|
|
2
|
-
import { machineKit } from './machine-ast.mjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The front-end Supabase result checks (FE_DB_RESULT_TYPED, FE_DB_ERROR_HANDLED): a query result keeps its inferred row type, a list read is
|
|
6
|
-
* bounded, and the awaited result is either passed whole to `toOutcome` or has its error read.
|
|
7
|
-
*/
|
|
8
|
-
const FE_RESULT_TYPED = 'FE_DB_RESULT_TYPED';
|
|
9
|
-
const FE_ERROR_HANDLED = 'FE_DB_ERROR_HANDLED';
|
|
10
|
-
export const FE_DB_SLOTS = new Set(['fe.modules.db', 'fe.package.db', 'fe.modules.db.outcome']);
|
|
11
|
-
const QUERY_TERMINALS = new Set(['single', 'maybeSingle', 'returns']);
|
|
12
|
-
const QUERY_WRITES = new Set(['insert', 'update', 'upsert', 'delete']);
|
|
13
|
-
const LIST_BOUNDS = new Set(['gt', 'gte', 'lt', 'lte', 'range']);
|
|
14
|
-
|
|
15
|
-
export const symbolOf = (kit, checker, node) => kit.aliased(checker, kit.symbolAt(checker, node)) ?? kit.symbolAt(checker, node);
|
|
16
|
-
|
|
17
|
-
export const sameSymbol = (kit, checker, node, symbol) => Boolean(symbol && symbolOf(kit, checker, node) === symbol);
|
|
18
|
-
|
|
19
|
-
const fromAwaitedSupabase = (kit, checker, node, seen = new Set()) => {
|
|
20
|
-
const current = unwrap(kit.ts, node);
|
|
21
|
-
if (!current || seen.has(current)) return false;
|
|
22
|
-
seen.add(current);
|
|
23
|
-
if (kit.ts.isCallExpression(current)) return isSupabaseCall(kit, checker, current);
|
|
24
|
-
if (!kit.ts.isIdentifier(current)) return false;
|
|
25
|
-
for (const declaration of kit.declarationsOf(checker, current)) {
|
|
26
|
-
if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer && fromAwaitedSupabase(kit, checker, declaration.initializer, seen)) return true;
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const callIsToOutcome = (kit, checker, graph, call) => {
|
|
32
|
-
if (!kit.ts.isCallExpression(call)) return false;
|
|
33
|
-
const expression = call.expression;
|
|
34
|
-
let name = null;
|
|
35
|
-
if (kit.ts.isIdentifier(expression)) name = expression.text;
|
|
36
|
-
else if (kit.ts.isPropertyAccessExpression(expression)) name = expression.name.text;
|
|
37
|
-
if (name !== 'toOutcome') return false;
|
|
38
|
-
return kit.declarationsOf(checker, kit.ts.isPropertyAccessExpression(expression) ? expression.name : expression)
|
|
39
|
-
.some(declaration => {
|
|
40
|
-
const rel = kit.graphPath(declaration);
|
|
41
|
-
return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const parentCall = (ts, node) => {
|
|
46
|
-
let current = node;
|
|
47
|
-
while (current.parent && ts.isParenthesizedExpression(current.parent)) current = current.parent;
|
|
48
|
-
return ts.isCallExpression(current.parent) ? current.parent : null;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** True when `node` reads `.error` / `['error']` of the result symbol, or passes the result whole to toOutcome. */
|
|
52
|
-
const readsResultError = (kit, checker, graph, node, result) => {
|
|
53
|
-
const { ts } = kit;
|
|
54
|
-
if (ts.isPropertyAccessExpression(node) && node.name.text === 'error' && sameSymbol(kit, checker, node.expression, result)) return true;
|
|
55
|
-
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === 'error' && sameSymbol(kit, checker, node.expression, result)) return true;
|
|
56
|
-
return ts.isCallExpression(node) && callIsToOutcome(kit, checker, graph, node)
|
|
57
|
-
&& node.arguments.some(argument => sameSymbol(kit, checker, unwrap(ts, argument), result));
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** A result bound to one identifier is handled when the file reads its error or hands it whole to toOutcome. */
|
|
61
|
-
const identifierResultHandled = (kit, checker, graph, awaitNode, declaration) => {
|
|
62
|
-
const result = symbolOf(kit, checker, declaration.name);
|
|
63
|
-
let handled = false;
|
|
64
|
-
kit.walk(awaitNode.getSourceFile(), node => {
|
|
65
|
-
if (readsResultError(kit, checker, graph, node, result)) handled = true;
|
|
66
|
-
return !handled;
|
|
67
|
-
});
|
|
68
|
-
return handled;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/** A result destructured into `{ error }` is handled when the bound error is referenced beyond its binding. */
|
|
72
|
-
const destructuredResultHandled = (kit, checker, awaitNode, declaration) => {
|
|
73
|
-
const errorBinding = declaration.name.elements.find(element => {
|
|
74
|
-
const property = element.propertyName ?? element.name;
|
|
75
|
-
return kit.ts.isIdentifier(property) && property.text === 'error';
|
|
76
|
-
});
|
|
77
|
-
if (!errorBinding || !kit.ts.isIdentifier(errorBinding.name)) return false;
|
|
78
|
-
const error = symbolOf(kit, checker, errorBinding.name);
|
|
79
|
-
let references = 0;
|
|
80
|
-
kit.walk(awaitNode.getSourceFile(), node => {
|
|
81
|
-
if (kit.ts.isIdentifier(node) && sameSymbol(kit, checker, node, error)) references += 1;
|
|
82
|
-
return true;
|
|
83
|
-
});
|
|
84
|
-
return references > 1;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handledAwait = (kit, checker, graph, awaitNode) => {
|
|
88
|
-
const direct = parentCall(kit.ts, awaitNode);
|
|
89
|
-
if (direct && callIsToOutcome(kit, checker, graph, direct) && direct.arguments.some(argument => argument === awaitNode || argument === awaitNode.parent)) return true;
|
|
90
|
-
let cursor = awaitNode;
|
|
91
|
-
while (cursor.parent && kit.ts.isParenthesizedExpression(cursor.parent)) cursor = cursor.parent;
|
|
92
|
-
const declaration = kit.ts.isVariableDeclaration(cursor.parent) ? cursor.parent : null;
|
|
93
|
-
if (!declaration) return false;
|
|
94
|
-
if (kit.ts.isIdentifier(declaration.name)) return identifierResultHandled(kit, checker, graph, awaitNode, declaration);
|
|
95
|
-
if (!kit.ts.isObjectBindingPattern(declaration.name)) return false;
|
|
96
|
-
return destructuredResultHandled(kit, checker, awaitNode, declaration);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const dataSymbolFromAwait = (kit, checker, identifier) => {
|
|
100
|
-
const symbol = symbolOf(kit, checker, identifier);
|
|
101
|
-
for (const declaration of symbol?.declarations ?? []) {
|
|
102
|
-
if (!kit.ts.isBindingElement(declaration) || !kit.ts.isObjectBindingPattern(declaration.parent)) continue;
|
|
103
|
-
const property = declaration.propertyName ?? declaration.name;
|
|
104
|
-
if (!kit.ts.isIdentifier(property) || property.text !== 'data') continue;
|
|
105
|
-
const variable = declaration.parent.parent;
|
|
106
|
-
if (kit.ts.isVariableDeclaration(variable) && variable.initializer && fromAwaitedSupabase(kit, checker, variable.initializer)) return symbol;
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
function inspectResultGeneric(scope, node) {
|
|
112
|
-
const { ts, kit, checker, report } = scope;
|
|
113
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && QUERY_TERMINALS.has(node.expression.name.text)
|
|
114
|
-
&& node.typeArguments?.length && isSupabaseValue(kit, checker, node.expression.expression)) {
|
|
115
|
-
report(FE_RESULT_TYPED, node, `${node.expression.name.text}<T>() supplies a result generic; derive the row type from Database and let the Supabase chain infer it.`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function inspectResultCast(scope, node) {
|
|
120
|
-
const { ts, kit, checker, report } = scope;
|
|
121
|
-
if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) && fromAwaitedSupabase(kit, checker, node.expression)) {
|
|
122
|
-
report(FE_RESULT_TYPED, node, 'A Supabase query result is cast or non-null asserted; narrow its typed data/error outcome instead.');
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Counts a multi-row read and reports it when no limit, range or keyset bound applies. */
|
|
127
|
-
function inspectListRead(scope, node, methods) {
|
|
128
|
-
const isListRead = methods.includes('select') && !methods.some(method => QUERY_WRITES.has(method)) && !methods.includes('single') && !methods.includes('maybeSingle');
|
|
129
|
-
if (!isListRead) return;
|
|
130
|
-
scope.counters.listReads += 1;
|
|
131
|
-
if (!methods.includes('limit') && !methods.some(method => LIST_BOUNDS.has(method))) {
|
|
132
|
-
scope.report(FE_RESULT_TYPED, node, 'A Supabase list read has no .limit(), .range(), or keyset bound (.gt/.gte/.lt/.lte); bound every multi-row read.');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function inspectAwaitedResult(scope, node) {
|
|
137
|
-
const { ts, kit, checker, report, graph, counters } = scope;
|
|
138
|
-
if (!ts.isAwaitExpression(node) || !isSupabaseValue(kit, checker, node.expression)) return;
|
|
139
|
-
const expression = unwrap(ts, node.expression);
|
|
140
|
-
if (!ts.isCallExpression(expression)) return;
|
|
141
|
-
counters.awaited += 1;
|
|
142
|
-
inspectListRead(scope, node, chainParts(ts, expression));
|
|
143
|
-
if (!handledAwait(kit, checker, graph, node)) {
|
|
144
|
-
report(FE_ERROR_HANDLED, node, 'The awaited Supabase result does not read its error and is not passed whole to toOutcome(result).');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function inspectSwallowedData(scope, node) {
|
|
149
|
-
const { ts, kit, checker, report } = scope;
|
|
150
|
-
if (!(ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && ts.isArrayLiteralExpression(node.right) && node.right.elements.length === 0)) return;
|
|
151
|
-
const left = unwrap(ts, node.left);
|
|
152
|
-
const swallowed = ts.isIdentifier(left) ? Boolean(dataSymbolFromAwait(kit, checker, left))
|
|
153
|
-
: ts.isPropertyAccessExpression(left) && left.name.text === 'data' && fromAwaitedSupabase(kit, checker, left.expression);
|
|
154
|
-
if (swallowed) report(FE_ERROR_HANDLED, node, 'Supabase data is collapsed with ?? []; refused or unavailable is not empty data. Pass the whole result to toOutcome.');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function inspectQueryResultNode(scope, node) {
|
|
158
|
-
inspectResultGeneric(scope, node);
|
|
159
|
-
inspectResultCast(scope, node);
|
|
160
|
-
inspectAwaitedResult(scope, node);
|
|
161
|
-
inspectSwallowedData(scope, node);
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function checkFrontendQueryResults(input) {
|
|
166
|
-
const kit = machineKit(input);
|
|
167
|
-
const violations = [];
|
|
168
|
-
const counters = { awaited: 0, listReads: 0 };
|
|
169
|
-
for (const file of input.graph.files.values()) {
|
|
170
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
171
|
-
const seen = new Set();
|
|
172
|
-
const report = (ruleId, node, message) => {
|
|
173
|
-
const key = `${ruleId}:${node.pos}:${message}`;
|
|
174
|
-
if (seen.has(key)) return;
|
|
175
|
-
seen.add(key);
|
|
176
|
-
reportAt(violations, kit, ruleId, file, node, message);
|
|
177
|
-
};
|
|
178
|
-
const scope = { ts: kit.ts, kit, checker, report, graph: input.graph, counters };
|
|
179
|
-
kit.walk(file.sourceFile, node => inspectQueryResultNode(scope, node));
|
|
180
|
-
}
|
|
181
|
-
return { violations, coverage: { status: 'checked', awaited: counters.awaited, listReads: counters.listReads } };
|
|
182
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { @@Feature@@HttpModule } from "./transport/http/@@feature@@-http.module"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Body, Controller, Post } from "@nestjs/common"
|
|
2
|
-
import type { CommandBus } from "@nestjs/cqrs"
|
|
3
|
-
import { CurrentPrincipal } from "@modules/domain/identity"
|
|
4
|
-
import { InjectCommandBus } from "@modules/platform/cqrs"
|
|
5
|
-
import type { Principal } from "@modules/platform/cqrs"
|
|
6
|
-
import { @@Action@@Command } from "../../application/@@action@@.command"
|
|
7
|
-
import { to@@Action@@Request, to@@Action@@Response } from "./@@action@@.mapper"
|
|
8
|
-
import { @@Action@@Request } from "./dto/@@action@@.request"
|
|
9
|
-
import { @@Action@@Response } from "./dto/@@action@@.response"
|
|
10
|
-
|
|
11
|
-
@Controller("@@feature@@")
|
|
12
|
-
/** HTTP door of @@actionCamel@@. */
|
|
13
|
-
export class @@Action@@Controller {
|
|
14
|
-
constructor(@InjectCommandBus() private readonly commandBus: CommandBus) {}
|
|
15
|
-
|
|
16
|
-
/** Runs @@action@@ for the authenticated caller. */
|
|
17
|
-
@Post()
|
|
18
|
-
async @@actionCamel@@(
|
|
19
|
-
@CurrentPrincipal() principal: Principal,
|
|
20
|
-
@Body() input: @@Action@@Request,
|
|
21
|
-
): Promise<@@Action@@Response> {
|
|
22
|
-
const result = await this.commandBus.execute(
|
|
23
|
-
new @@Action@@Command({ request: to@@Action@@Request(input), principal }),
|
|
24
|
-
)
|
|
25
|
-
return to@@Action@@Response(result)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { @@Action@@Request, @@Action@@Result } from "../../application/@@action@@.contracts"
|
|
2
|
-
import type { @@Action@@Request as @@Action@@HttpRequest } from "./dto/@@action@@.request"
|
|
3
|
-
import type { @@Action@@Response } from "./dto/@@action@@.response"
|
|
4
|
-
|
|
5
|
-
/** Maps the HTTP request to the command request. */
|
|
6
|
-
export const to@@Action@@Request = (input: @@Action@@HttpRequest): @@Action@@Request => ({ id: input.id })
|
|
7
|
-
|
|
8
|
-
/** Maps the command result to the HTTP response. */
|
|
9
|
-
export const to@@Action@@Response = (result: @@Action@@Result): @@Action@@Response => ({ id: result.id })
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Module } from "@nestjs/common"
|
|
2
|
-
import { @@Feature@@ApiModule } from "../../@@feature@@.module"
|
|
3
|
-
import { @@Action@@Controller } from "./@@action@@.controller"
|
|
4
|
-
|
|
5
|
-
@Module({ imports: [@@Feature@@ApiModule], controllers: [@@Action@@Controller] })
|
|
6
|
-
/** The HTTP transport of the @@feature@@ feature. */
|
|
7
|
-
export class @@Feature@@HttpModule {}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import type { NextRequest } from "next/server"
|
|
4
|
-
import { NextResponse } from "next/server"
|
|
5
|
-
import { exchangeAuthCode } from "@/modules/db"
|
|
6
|
-
import { safeNextPath } from "@/modules/routes"
|
|
7
|
-
|
|
8
|
-
const destinationOf = (request: NextRequest): URL => {
|
|
9
|
-
const path = safeNextPath(request.nextUrl.searchParams.get("next"), request.nextUrl.origin)
|
|
10
|
-
return new URL(path, request.nextUrl.origin)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Completes Supabase's PKCE exchange and returns only to a same-origin path. */
|
|
14
|
-
export const GET = async (request: NextRequest): Promise<NextResponse> => {
|
|
15
|
-
const destination = destinationOf(request)
|
|
16
|
-
const code = request.nextUrl.searchParams.get("code")
|
|
17
|
-
if (code === null) {
|
|
18
|
-
destination.searchParams.set("auth", "invalid")
|
|
19
|
-
return NextResponse.redirect(destination)
|
|
20
|
-
}
|
|
21
|
-
const exchanged = await exchangeAuthCode(code)
|
|
22
|
-
if (exchanged.kind !== "ok") destination.searchParams.set("auth", exchanged.kind)
|
|
23
|
-
return NextResponse.redirect(destination)
|
|
24
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useTranslations } from "next-intl"
|
|
4
|
-
import { useState, useTransition } from "react"
|
|
5
|
-
import { writeSignOut } from "@/modules/db/auth/write-sign-out"
|
|
6
|
-
import { navigation } from "@/modules/i18n"
|
|
7
|
-
import { SignOutButtonBase } from "./component"
|
|
8
|
-
|
|
9
|
-
type SignOutFailure = "refused" | "not-found" | "invalid" | "unavailable"
|
|
10
|
-
|
|
11
|
-
/** The connected control that ends the current browser session. */
|
|
12
|
-
export const SignOutButton = () => {
|
|
13
|
-
const router = navigation.useRouter()
|
|
14
|
-
const t = useTranslations("app.signOut")
|
|
15
|
-
const [failure, setFailure] = useState<SignOutFailure>()
|
|
16
|
-
const [pending, startTransition] = useTransition()
|
|
17
|
-
const signOut = () => {
|
|
18
|
-
startTransition(async () => {
|
|
19
|
-
const outcome = await writeSignOut()
|
|
20
|
-
if (outcome.kind !== "ok") {
|
|
21
|
-
setFailure(outcome.kind)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
setFailure(undefined)
|
|
25
|
-
router.refresh()
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
return (
|
|
29
|
-
<SignOutButtonBase
|
|
30
|
-
props={{
|
|
31
|
-
label: pending ? t("working") : t("submit"),
|
|
32
|
-
pending,
|
|
33
|
-
failure: failure === undefined ? undefined : t(failure),
|
|
34
|
-
}}
|
|
35
|
-
on={{ press: signOut }}
|
|
36
|
-
/>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import type { NextRequest, NextResponse } from "next/server"
|
|
4
|
-
import { getPrincipal, principalFromClaims } from "../principal"
|
|
5
|
-
import type { Principal } from "../principal"
|
|
6
|
-
import { createRequestDbClient, createServerDbClient } from "../server"
|
|
7
|
-
import { toOutcome } from "../outcome"
|
|
8
|
-
import type { DbOutcome } from "../outcome"
|
|
9
|
-
|
|
10
|
-
/** Reads the verified principal of the current server request. */
|
|
11
|
-
export const readSession = (): Promise<DbOutcome<Principal>> => getPrincipal()
|
|
12
|
-
|
|
13
|
-
/** Exchanges the one-time PKCE code and lets the server client write the resulting cookies. */
|
|
14
|
-
export const exchangeAuthCode = async (code: string) => {
|
|
15
|
-
const client = await createServerDbClient()
|
|
16
|
-
return toOutcome(await client.auth.exchangeCodeForSession(code))
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Refreshes proxy cookies through `getClaims()` and returns the verified principal outcome. */
|
|
20
|
-
export const refreshSession = async (request: NextRequest, response: NextResponse): Promise<DbOutcome<Principal>> => {
|
|
21
|
-
const client = createRequestDbClient(request, response)
|
|
22
|
-
const claims = toOutcome(await client.auth.getClaims())
|
|
23
|
-
if (claims.kind !== "ok") return claims
|
|
24
|
-
return principalFromClaims(claims.value.claims)
|
|
25
|
-
}
|