@percepta/create 4.1.15 → 4.1.16
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/package.json +5 -2
- package/templates/monorepo/auth/package.json +1 -1
- package/templates/monorepo/auth/src/auth.ts +8 -102
- package/templates/monorepo/oxlint.config.ts.template +5 -1
- package/templates/monorepo/package.json.template +2 -1
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +22 -89
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/agent-skills/langfuse.md +8 -11
- package/templates/webapp/package.json.template +4 -5
- package/templates/webapp/scripts/seed.ts +29 -22
- package/templates/webapp/scripts/with-local-env.ts +12 -64
- package/templates/webapp/src/drizzle/db.ts +5 -9
- package/templates/webapp/src/instrumentation.ts +5 -72
- package/templates/webapp/src/lib/auth/app-auth-mode.ts +9 -17
- package/templates/webapp/src/services/DatabaseService.ts +3 -51
- package/templates/webapp/src/services/observability/initFaro.ts +5 -17
- package/templates/monorepo/scripts/setup-local-databases.mjs +0 -183
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percepta/create",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.16",
|
|
4
4
|
"description": "Scaffold a new Mosaic package",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"test": "vitest run",
|
|
52
52
|
"test:watch": "vitest",
|
|
53
|
-
"
|
|
53
|
+
"lint:templates:logging": "oxlint -c oxlint.template-logging.config.json templates --no-error-on-unmatched-pattern",
|
|
54
|
+
"test:template": "pnpm lint:templates:logging && pnpm test:template:contract && pnpm build && pnpm test:template:build",
|
|
55
|
+
"test:template:contract": "vitest run src/commands/create-output.test.ts",
|
|
56
|
+
"test:template:build": "bash scripts/test-template.sh",
|
|
54
57
|
"create:local": "pnpm build && node dist/index.js",
|
|
55
58
|
"sync-template": "tsx scripts/sync-template.ts",
|
|
56
59
|
"template:tag": "tsx scripts/template-tag.ts"
|
|
@@ -1,120 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
createLazyAuth,
|
|
3
|
+
createPerceptaAuthFromEnv,
|
|
4
|
+
type PerceptaAuthMode,
|
|
5
|
+
} from "@percepta/auth/better-auth";
|
|
5
6
|
import { db } from "./drizzle/db";
|
|
6
7
|
import { accounts } from "./drizzle/schema/auth/accounts";
|
|
7
8
|
import { sessions } from "./drizzle/schema/auth/sessions";
|
|
8
9
|
import { verifications } from "./drizzle/schema/auth/verifications";
|
|
9
10
|
import { users } from "./drizzle/schema/users";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const DEFAULT_AUTH_MODE: AuthMode = "__AUTH_MODE__" as AuthMode;
|
|
14
|
-
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
|
15
|
-
|
|
16
|
-
function isAuthMode(value: string | undefined): value is AuthMode {
|
|
17
|
-
return (
|
|
18
|
-
value === "username-password" || value === "google" || value === "okta"
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getAuthMode(): AuthMode {
|
|
23
|
-
return isAuthMode(process.env.AUTH_MODE)
|
|
24
|
-
? process.env.AUTH_MODE
|
|
25
|
-
: DEFAULT_AUTH_MODE;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function requiredEnv(name: string): string {
|
|
29
|
-
const value = process.env[name];
|
|
30
|
-
if (value == null || value.length === 0) {
|
|
31
|
-
throw new Error(`${name} is required.`);
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function optionalEnv(name: string): string | undefined {
|
|
37
|
-
const value = process.env[name];
|
|
38
|
-
return value == null || value.length === 0 ? undefined : value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getSecret(): string {
|
|
42
|
-
if (isBuildPhase) {
|
|
43
|
-
return "build-placeholder-not-used-at-runtime";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return requiredEnv("BETTER_AUTH_SECRET");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getBaseUrl(): string {
|
|
50
|
-
return (
|
|
51
|
-
process.env.BETTER_AUTH_URL ??
|
|
52
|
-
process.env.APP_BASE_URL ??
|
|
53
|
-
"http://localhost:3000"
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getSocialProviders(
|
|
58
|
-
authMode: AuthMode,
|
|
59
|
-
): BetterAuthOptions["socialProviders"] {
|
|
60
|
-
if (authMode !== "google") return undefined;
|
|
61
|
-
|
|
62
|
-
const clientId = optionalEnv("GOOGLE_CLIENT_ID");
|
|
63
|
-
const clientSecret = optionalEnv("GOOGLE_CLIENT_SECRET");
|
|
64
|
-
if (clientId == null || clientSecret == null) return undefined;
|
|
65
|
-
|
|
66
|
-
const hostedDomain = optionalEnv("GOOGLE_HOSTED_DOMAIN");
|
|
67
|
-
return {
|
|
68
|
-
google: {
|
|
69
|
-
clientId,
|
|
70
|
-
clientSecret,
|
|
71
|
-
...(hostedDomain == null ? {} : { hd: hostedDomain }),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getPlugins(authMode: AuthMode): BetterAuthOptions["plugins"] {
|
|
77
|
-
if (authMode !== "okta") return undefined;
|
|
78
|
-
|
|
79
|
-
const clientId = optionalEnv("OKTA_CLIENT_ID");
|
|
80
|
-
const clientSecret = optionalEnv("OKTA_CLIENT_SECRET");
|
|
81
|
-
const issuer = optionalEnv("OKTA_ISSUER");
|
|
82
|
-
if (clientId == null || clientSecret == null || issuer == null) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
admin(),
|
|
88
|
-
genericOAuth({
|
|
89
|
-
config: [
|
|
90
|
-
okta({
|
|
91
|
-
clientId,
|
|
92
|
-
clientSecret,
|
|
93
|
-
issuer,
|
|
94
|
-
}),
|
|
95
|
-
],
|
|
96
|
-
}),
|
|
97
|
-
];
|
|
98
|
-
}
|
|
12
|
+
const DEFAULT_AUTH_MODE = "__AUTH_MODE__" satisfies PerceptaAuthMode;
|
|
99
13
|
|
|
100
14
|
function createAuth() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return createPerceptaAuth({
|
|
104
|
-
baseURL: getBaseUrl(),
|
|
15
|
+
return createPerceptaAuthFromEnv({
|
|
105
16
|
database: db,
|
|
106
|
-
|
|
107
|
-
enabled: authMode === "username-password",
|
|
108
|
-
},
|
|
109
|
-
plugins: getPlugins(authMode),
|
|
17
|
+
defaultAuthMode: DEFAULT_AUTH_MODE,
|
|
110
18
|
schema: {
|
|
111
19
|
user: users,
|
|
112
20
|
session: sessions,
|
|
113
21
|
account: accounts,
|
|
114
22
|
verification: verifications,
|
|
115
23
|
},
|
|
116
|
-
secret: getSecret(),
|
|
117
|
-
socialProviders: getSocialProviders(authMode),
|
|
118
24
|
});
|
|
119
25
|
}
|
|
120
26
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"setup": "pnpm run docker:up && pnpm run db:setup-local && pnpm run auth:db:migrate && pnpm run access:apply-local && pnpm -r --filter './packages/*' --if-present run db:migrate && pnpm -r --filter './packages/*' --if-present run db:seed",
|
|
11
11
|
"docker:up": "docker compose up -d --wait",
|
|
12
12
|
"docker:down": "docker compose down",
|
|
13
|
-
"db:setup-local": "
|
|
13
|
+
"db:setup-local": "percepta-db setup-local",
|
|
14
14
|
"dev": "pnpm -r --parallel --if-present run dev",
|
|
15
15
|
"build": "turbo run build",
|
|
16
16
|
"clean": "turbo run clean",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@percepta/access-control": "^1.0.0",
|
|
34
34
|
"@percepta/build": "^1.0.0",
|
|
35
|
+
"@percepta/database": "0.1.4",
|
|
35
36
|
"@types/node": "^24.1.0",
|
|
36
37
|
"oxfmt": "^0.47.0",
|
|
37
38
|
"oxlint": "^1.61.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
name: Build & Release __APP_NAME__
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
workflow_dispatch:
|
|
4
5
|
push:
|
|
5
6
|
branches:
|
|
6
7
|
- "main"
|
|
@@ -13,97 +14,29 @@ on:
|
|
|
13
14
|
- "pnpm-lock.yaml"
|
|
14
15
|
- "pnpm-workspace.yaml"
|
|
15
16
|
- ".github/workflows/__APP_NAME__-ryvn-release.yaml"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
pull_request:
|
|
18
|
+
paths:
|
|
19
|
+
- "packages/__APP_NAME__/src/**"
|
|
20
|
+
- "packages/__APP_NAME__/scripts/**"
|
|
21
|
+
- "packages/__APP_NAME__/Dockerfile"
|
|
22
|
+
- "packages/__APP_NAME__/package.json"
|
|
23
|
+
- "package.json"
|
|
24
|
+
- "pnpm-lock.yaml"
|
|
25
|
+
- "pnpm-workspace.yaml"
|
|
26
|
+
- ".github/workflows/__APP_NAME__-ryvn-release.yaml"
|
|
20
27
|
|
|
21
28
|
jobs:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
release:
|
|
30
|
+
uses: ryvn-technologies/ryvn-build-action/.github/workflows/release.yml@v2
|
|
31
|
+
with:
|
|
32
|
+
service_name: __APP_NAME__
|
|
25
33
|
permissions:
|
|
26
34
|
contents: write
|
|
27
35
|
id-token: write
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- name: Install Ryvn CLI
|
|
36
|
-
uses: ryvn-technologies/install-ryvn-cli@v1.0.0
|
|
37
|
-
|
|
38
|
-
- name: Generate Release Tag
|
|
39
|
-
id: generate-tag
|
|
40
|
-
env:
|
|
41
|
-
RYVN_CLIENT_ID: ${{ secrets.RYVN_CLIENT_ID }}
|
|
42
|
-
RYVN_CLIENT_SECRET: ${{ secrets.RYVN_CLIENT_SECRET }}
|
|
43
|
-
run: |
|
|
44
|
-
tag_info=$(ryvn generate-release-tag "$SERVICE_NAME" --prefix="${SERVICE_NAME}@" -o json --default-bump-minor)
|
|
45
|
-
|
|
46
|
-
version=$(echo "$tag_info" | jq -r '.version')
|
|
47
|
-
new_tag=$(echo "$tag_info" | jq -r '.tag')
|
|
48
|
-
channel=$(echo "$tag_info" | jq -r '.channel')
|
|
49
|
-
isPreview=$(echo "$tag_info" | jq -r '.isPreview')
|
|
50
|
-
|
|
51
|
-
echo "version=$version" >> $GITHUB_OUTPUT
|
|
52
|
-
echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
|
|
53
|
-
echo "channel=$channel" >> $GITHUB_OUTPUT
|
|
54
|
-
echo "isPreview=$isPreview" >> $GITHUB_OUTPUT
|
|
55
|
-
|
|
56
|
-
- name: Build and Push
|
|
57
|
-
uses: ryvn-technologies/ryvn-build-action@v2
|
|
58
|
-
with:
|
|
59
|
-
service_name: ${{ env.SERVICE_NAME }}
|
|
60
|
-
version: ${{ steps.generate-tag.outputs.version }}
|
|
61
|
-
build_only: ${{ !(github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || steps.generate-tag.outputs.isPreview == 'true') }}
|
|
62
|
-
build_secrets: |
|
|
63
|
-
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
|
64
|
-
ryvn_client_id: ${{ secrets.RYVN_CLIENT_ID }}
|
|
65
|
-
ryvn_client_secret: ${{ secrets.RYVN_CLIENT_SECRET }}
|
|
66
|
-
|
|
67
|
-
- name: Create Ryvn Release
|
|
68
|
-
if: |
|
|
69
|
-
!contains(github.event.head_commit.message, '[skip-release]') &&
|
|
70
|
-
!contains(github.event.pull_request.title, '[skip-release]') &&
|
|
71
|
-
(steps.generate-tag.outputs.isPreview == 'true' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
|
|
72
|
-
env:
|
|
73
|
-
RYVN_CLIENT_ID: ${{ secrets.RYVN_CLIENT_ID }}
|
|
74
|
-
RYVN_CLIENT_SECRET: ${{ secrets.RYVN_CLIENT_SECRET }}
|
|
75
|
-
run: |
|
|
76
|
-
version="${{ steps.generate-tag.outputs.new_tag }}"
|
|
77
|
-
version="${version#"${SERVICE_NAME}@"}"
|
|
78
|
-
version="${version#@}"
|
|
79
|
-
channel="${{ steps.generate-tag.outputs.channel }}"
|
|
80
|
-
|
|
81
|
-
if [ -n "$channel" ] && [ "$channel" != "null" ]; then
|
|
82
|
-
ryvn create release "$SERVICE_NAME" "$version" --channel "$channel"
|
|
83
|
-
else
|
|
84
|
-
ryvn create release "$SERVICE_NAME" "$version"
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
- name: Create GitHub Tag
|
|
88
|
-
if: |
|
|
89
|
-
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
|
|
90
|
-
!contains(github.event.head_commit.message, '[skip-release]') &&
|
|
91
|
-
!contains(github.event.pull_request.title, '[skip-release]')
|
|
92
|
-
run: |
|
|
93
|
-
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
94
|
-
git config --global user.name "github-actions[bot]"
|
|
95
|
-
git tag "${{ steps.generate-tag.outputs.new_tag }}"
|
|
96
|
-
git push origin "${{ steps.generate-tag.outputs.new_tag }}"
|
|
97
|
-
|
|
98
|
-
- name: Create GitHub Release
|
|
99
|
-
if: |
|
|
100
|
-
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
|
|
101
|
-
!contains(github.event.head_commit.message, '[skip-release]') &&
|
|
102
|
-
!contains(github.event.pull_request.title, '[skip-release]')
|
|
103
|
-
uses: softprops/action-gh-release@v1
|
|
104
|
-
with:
|
|
105
|
-
tag_name: ${{ steps.generate-tag.outputs.new_tag }}
|
|
106
|
-
name: ${{ steps.generate-tag.outputs.new_tag }}
|
|
107
|
-
generate_release_notes: true
|
|
108
|
-
draft: false
|
|
109
|
-
prerelease: false
|
|
36
|
+
secrets:
|
|
37
|
+
RYVN_CLIENT_ID: ${{ secrets.RYVN_CLIENT_ID }}
|
|
38
|
+
RYVN_CLIENT_SECRET: ${{ secrets.RYVN_CLIENT_SECRET }}
|
|
39
|
+
BUILD_SECRETS: |
|
|
40
|
+
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
|
41
|
+
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
|
|
42
|
+
TURBO_TEAM=${{ vars.TURBO_TEAM }}
|
|
@@ -54,7 +54,7 @@ src/ # Application source
|
|
|
54
54
|
│ ├── langfuse/ # LLM observability
|
|
55
55
|
│ ├── llm/ # LLM provider selection and call helpers
|
|
56
56
|
│ ├── logger/ # App logger setup (wraps @percepta/logger)
|
|
57
|
-
│ └── observability/ #
|
|
57
|
+
│ └── observability/ # Frontend observability setup
|
|
58
58
|
└── utils/ # Helpers (cn, pathEncryption, etc.)
|
|
59
59
|
|
|
60
60
|
deploy/ # Optional release metadata
|
|
@@ -22,18 +22,15 @@ Langfuse is an open-source LLM observability platform. It captures traces, spans
|
|
|
22
22
|
The template uses Next.js's instrumentation hook (called on server startup) to bootstrap OTEL with both the environment collector and optional Langfuse:
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
],
|
|
34
|
-
instrumentations: [getNodeAutoInstrumentations()],
|
|
25
|
+
import { startPerceptaNodeTelemetry } from "@percepta/ai";
|
|
26
|
+
import { getEnvConfig } from "./config/getEnvConfig";
|
|
27
|
+
import { getLogger } from "./services/logger/AppLogger";
|
|
28
|
+
|
|
29
|
+
startPerceptaNodeTelemetry({
|
|
30
|
+
appName: "__APP_NAME__",
|
|
31
|
+
getEnv: getEnvConfig,
|
|
32
|
+
getLogger,
|
|
35
33
|
});
|
|
36
|
-
sdk.start();
|
|
37
34
|
```
|
|
38
35
|
|
|
39
36
|
- `getNodeAutoInstrumentations()` automatically instruments HTTP calls, database queries, and other standard Node.js operations.
|
|
@@ -40,17 +40,16 @@
|
|
|
40
40
|
"@mantine/hooks": "^8.3.1",
|
|
41
41
|
"@next/env": "^16.2.6",
|
|
42
42
|
"@opentelemetry/api": "^1.9.0",
|
|
43
|
-
"@opentelemetry/auto-instrumentations-node": "^0.75.0",
|
|
44
|
-
"@opentelemetry/exporter-trace-otlp-proto": "^0.217.0",
|
|
45
43
|
"@opentelemetry/sdk-node": "^0.217.0",
|
|
46
44
|
"@__REPO_NAME__/auth": "workspace:*",
|
|
47
45
|
"@percepta/access-control": "^1.0.0",
|
|
48
|
-
"@percepta/ai": "^0.1.
|
|
49
|
-
"@percepta/
|
|
46
|
+
"@percepta/ai": "^0.1.1",
|
|
47
|
+
"@percepta/auth": "^0.1.7",
|
|
48
|
+
"@percepta/database": "0.1.4",
|
|
50
49
|
"@percepta/design": "^0.4.1",
|
|
51
50
|
"@percepta/inngest": "^0.1.0",
|
|
52
51
|
"@percepta/logger": "^0.1.0",
|
|
53
|
-
"@percepta/next-utils": "^0.2.
|
|
52
|
+
"@percepta/next-utils": "^0.2.3",
|
|
54
53
|
"@percepta/utils": "^0.1.11",
|
|
55
54
|
"@radix-ui/react-slot": "^1.2.3",
|
|
56
55
|
"@tanstack/react-query": "^5.81.5",
|
|
@@ -11,6 +11,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
12
|
import * as nextEnvModule from "@next/env";
|
|
13
13
|
import type { SubjectRef } from "@percepta/access-control";
|
|
14
|
+
import {
|
|
15
|
+
ensurePerceptaAuthModeEnv,
|
|
16
|
+
type PerceptaAuthMode,
|
|
17
|
+
} from "@percepta/auth/better-auth";
|
|
14
18
|
|
|
15
19
|
const nextEnv =
|
|
16
20
|
(nextEnvModule as { default?: typeof nextEnvModule }).default ??
|
|
@@ -47,7 +51,6 @@ const SEEDED_USERS = [
|
|
|
47
51
|
},
|
|
48
52
|
] as const;
|
|
49
53
|
|
|
50
|
-
type AuthMode = "username-password" | "google" | "okta";
|
|
51
54
|
interface AdminCreateUserApi {
|
|
52
55
|
createUser(input: {
|
|
53
56
|
body: {
|
|
@@ -58,24 +61,13 @@ interface AdminCreateUserApi {
|
|
|
58
61
|
}): Promise<{ user: { id: string } }>;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
const DEFAULT_AUTH_MODE
|
|
62
|
-
|
|
63
|
-
function isAuthMode(value: string | undefined): value is AuthMode {
|
|
64
|
-
return (
|
|
65
|
-
value === "username-password" || value === "google" || value === "okta"
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getAuthMode(): AuthMode {
|
|
70
|
-
return isAuthMode(process.env.AUTH_MODE)
|
|
71
|
-
? process.env.AUTH_MODE
|
|
72
|
-
: DEFAULT_AUTH_MODE;
|
|
73
|
-
}
|
|
64
|
+
const DEFAULT_AUTH_MODE = "__AUTH_MODE__" satisfies PerceptaAuthMode;
|
|
74
65
|
|
|
75
66
|
async function main(): Promise<void> {
|
|
76
67
|
nextEnv.loadEnvConfig(process.cwd());
|
|
77
|
-
const authMode =
|
|
78
|
-
|
|
68
|
+
const authMode = ensurePerceptaAuthModeEnv({
|
|
69
|
+
defaultAuthMode: DEFAULT_AUTH_MODE,
|
|
70
|
+
});
|
|
79
71
|
// oxlint-disable-next-line typescript/no-explicit-any
|
|
80
72
|
(globalThis as any).AsyncLocalStorage = AsyncLocalStorage;
|
|
81
73
|
|
|
@@ -85,10 +77,12 @@ async function main(): Promise<void> {
|
|
|
85
77
|
const { getAccessControl, toUserSubject } =
|
|
86
78
|
await import("../src/services/access/AppAccessControl");
|
|
87
79
|
const { getEnvConfig } = await import("../src/config/getEnvConfig");
|
|
80
|
+
const { getLogger } = await import("../src/services/logger/AppLogger");
|
|
88
81
|
const { createCustomerAccessControl } =
|
|
89
82
|
await import("@percepta/access-control");
|
|
90
83
|
const { eq, sql } = await import("drizzle-orm");
|
|
91
84
|
|
|
85
|
+
const logger = getLogger().child({ safe: { component: "db-seed" } });
|
|
92
86
|
const envConfig = getEnvConfig();
|
|
93
87
|
const access = getAccessControl();
|
|
94
88
|
const appNamespace = access.manifest.appNamespace;
|
|
@@ -108,8 +102,9 @@ async function main(): Promise<void> {
|
|
|
108
102
|
let userId: string;
|
|
109
103
|
if (existing != null) {
|
|
110
104
|
userId = existing.id;
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
logger.info(
|
|
106
|
+
{ safe: { email: seededUser.email, userId: existing.id } },
|
|
107
|
+
"Seed user already exists.",
|
|
113
108
|
);
|
|
114
109
|
} else {
|
|
115
110
|
if (authMode === "username-password") {
|
|
@@ -135,9 +130,18 @@ async function main(): Promise<void> {
|
|
|
135
130
|
userId = res.user.id;
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
|
|
133
|
+
logger.info(
|
|
134
|
+
{ safe: { email: seededUser.email, userId } },
|
|
135
|
+
"Seed user created.",
|
|
136
|
+
);
|
|
139
137
|
if (authMode === "username-password") {
|
|
140
|
-
|
|
138
|
+
logger.info(
|
|
139
|
+
{
|
|
140
|
+
safe: { email: seededUser.email },
|
|
141
|
+
unsafe: { password: seededUser.password },
|
|
142
|
+
},
|
|
143
|
+
"Seed user password configured.",
|
|
144
|
+
);
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
|
|
@@ -145,7 +149,10 @@ async function main(): Promise<void> {
|
|
|
145
149
|
.update(users)
|
|
146
150
|
.set({ role: seededUser.role })
|
|
147
151
|
.where(eq(users.id, userId));
|
|
148
|
-
|
|
152
|
+
logger.info(
|
|
153
|
+
{ safe: { email: seededUser.email, role: seededUser.role } },
|
|
154
|
+
"Seed user role ensured.",
|
|
155
|
+
);
|
|
149
156
|
|
|
150
157
|
const subject = toUserSubject(userId);
|
|
151
158
|
switch (seededUser.access) {
|
|
@@ -169,7 +176,7 @@ async function main(): Promise<void> {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
|
|
179
|
+
logger.info(undefined, "Ensured local customer and app access grants.");
|
|
173
180
|
process.exit(0);
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -1,75 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
3
|
import { homedir } from "node:os";
|
|
6
4
|
import path from "node:path";
|
|
5
|
+
import { runCommandWithEnvFile } from "@percepta/database";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const LOCAL_ENV_PATH = path.join(
|
|
11
|
-
homedir(),
|
|
12
|
-
".config",
|
|
13
|
-
"percepta",
|
|
14
|
-
"create.env",
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
function parseEnvFile(filePath: string): EnvMap {
|
|
18
|
-
if (!existsSync(filePath)) return {};
|
|
19
|
-
|
|
20
|
-
const env: EnvMap = {};
|
|
21
|
-
const content = readFileSync(filePath, "utf8");
|
|
22
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
23
|
-
const line = rawLine.trim();
|
|
24
|
-
if (!line || line.startsWith("#")) continue;
|
|
25
|
-
|
|
26
|
-
const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
27
|
-
const separatorIndex = normalized.indexOf("=");
|
|
28
|
-
if (separatorIndex === -1) continue;
|
|
29
|
-
|
|
30
|
-
const key = normalized.slice(0, separatorIndex).trim();
|
|
31
|
-
const rawValue = normalized.slice(separatorIndex + 1).trim();
|
|
32
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
33
|
-
|
|
34
|
-
env[key] = unquote(rawValue);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return env;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function unquote(value: string): string {
|
|
41
|
-
if (
|
|
42
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
43
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
44
|
-
) {
|
|
45
|
-
return value.slice(1, -1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
7
|
+
const LOCAL_ENV_DIR = path.join(homedir(), ".config", "percepta");
|
|
8
|
+
const LOCAL_ENV_FILE = "create.env";
|
|
50
9
|
|
|
51
10
|
const [command, ...args] = process.argv.slice(2);
|
|
52
11
|
if (!command) {
|
|
53
12
|
throw new Error("Usage: tsx scripts/with-local-env.ts <command> [...args]");
|
|
54
13
|
}
|
|
55
14
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw error;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
child.on("exit", (code, signal) => {
|
|
69
|
-
if (signal) {
|
|
70
|
-
process.kill(process.pid, signal);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
process.exit(code ?? 1);
|
|
75
|
-
});
|
|
15
|
+
process.exit(
|
|
16
|
+
await runCommandWithEnvFile({
|
|
17
|
+
args,
|
|
18
|
+
allowedEnvFileNames: [LOCAL_ENV_FILE],
|
|
19
|
+
command,
|
|
20
|
+
envFileBaseDir: LOCAL_ENV_DIR,
|
|
21
|
+
envFilePath: LOCAL_ENV_FILE,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createDrizzlePgDatabase } from "@percepta/database";
|
|
2
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
3
|
import type { Pool } from "pg";
|
|
4
4
|
import { getEnvConfig } from "../config/getEnvConfig";
|
|
5
5
|
|
|
@@ -7,11 +7,7 @@ export const { client, db } = createDb();
|
|
|
7
7
|
|
|
8
8
|
function createDb(): { client: Pool; db: NodePgDatabase } {
|
|
9
9
|
const { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv } = getEnvConfig();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
return { client: pool, db: drizzle(pool) };
|
|
10
|
+
return createDrizzlePgDatabase({
|
|
11
|
+
env: { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv },
|
|
12
|
+
});
|
|
17
13
|
}
|
|
@@ -1,76 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
3
|
-
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
4
|
-
import { createLangfuseSpanProcessor } from "@percepta/ai";
|
|
5
|
-
import { compact } from "lodash-es";
|
|
1
|
+
import { startPerceptaNodeTelemetry } from "@percepta/ai";
|
|
6
2
|
import { getEnvConfig } from "./config/getEnvConfig";
|
|
7
3
|
import { getLogger } from "./services/logger/AppLogger";
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
getEnvConfig();
|
|
14
|
-
|
|
15
|
-
process.env.OTEL_SERVICE_NAME ??= "__APP_NAME__";
|
|
16
|
-
process.env.OTEL_RESOURCE_ATTRIBUTES ??= `deployment.environment=${deploymentEnvironment ?? nodeEnv}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getOtlpTracesEndpoint(): string | undefined {
|
|
20
|
-
const {
|
|
21
|
-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: tracesEndpoint,
|
|
22
|
-
OTEL_EXPORTER_OTLP_ENDPOINT: baseEndpoint,
|
|
23
|
-
} = getEnvConfig();
|
|
24
|
-
|
|
25
|
-
if (tracesEndpoint) return tracesEndpoint;
|
|
26
|
-
|
|
27
|
-
if (!baseEndpoint) return undefined;
|
|
28
|
-
|
|
29
|
-
return `${baseEndpoint.replace(/\/$/, "")}/v1/traces`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
|
|
33
|
-
const { OTEL_TRACES_EXPORTER: tracesExporter } = getEnvConfig();
|
|
34
|
-
if (tracesExporter === "none") {
|
|
35
|
-
getLogger().debug(
|
|
36
|
-
undefined,
|
|
37
|
-
"OTEL_TRACES_EXPORTER=none. Skipping OTLP trace export.",
|
|
38
|
-
);
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const tracesEndpoint = getOtlpTracesEndpoint();
|
|
43
|
-
if (!tracesEndpoint) {
|
|
44
|
-
getLogger().debug(
|
|
45
|
-
undefined,
|
|
46
|
-
"No OTLP trace endpoint found. Skipping OTLP trace export.",
|
|
47
|
-
);
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getLogger().debug(
|
|
52
|
-
{ safe: { tracesEndpoint } },
|
|
53
|
-
"Registering OTLP trace exporter.",
|
|
54
|
-
);
|
|
55
|
-
return new tracing.BatchSpanProcessor(
|
|
56
|
-
new OTLPTraceExporter({ url: tracesEndpoint }),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getLangfuseSpanProcessor(): SpanProcessor | undefined {
|
|
61
|
-
return createLangfuseSpanProcessor(getEnvConfig(), getLogger());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
setDefaultOpenTelemetryEnv();
|
|
65
|
-
|
|
66
|
-
const spanProcessors: tracing.SpanProcessor[] = compact([
|
|
67
|
-
getOtlpSpanProcessor(),
|
|
68
|
-
getLangfuseSpanProcessor(),
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
const sdk = new NodeSDK({
|
|
72
|
-
spanProcessors,
|
|
73
|
-
instrumentations: [getNodeAutoInstrumentations()],
|
|
5
|
+
startPerceptaNodeTelemetry({
|
|
6
|
+
appName: "__APP_NAME__",
|
|
7
|
+
getEnv: getEnvConfig,
|
|
8
|
+
getLogger,
|
|
74
9
|
});
|
|
75
|
-
|
|
76
|
-
sdk.start();
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
ensurePerceptaAuthModeEnv,
|
|
3
|
+
type PerceptaAuthMode,
|
|
4
|
+
} from "@percepta/auth/better-auth";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function isAuthMode(value: string | undefined): value is AuthMode {
|
|
6
|
-
return (
|
|
7
|
-
value === "username-password" || value === "google" || value === "okta"
|
|
8
|
-
);
|
|
9
|
-
}
|
|
6
|
+
export type AuthMode = PerceptaAuthMode;
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
if (isAuthMode(process.env.AUTH_MODE)) {
|
|
13
|
-
return process.env.AUTH_MODE;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
process.env.AUTH_MODE = APP_AUTH_MODE;
|
|
17
|
-
return APP_AUTH_MODE;
|
|
18
|
-
}
|
|
8
|
+
const APP_AUTH_MODE: AuthMode = "__AUTH_MODE__" as AuthMode;
|
|
19
9
|
|
|
20
|
-
export const AUTH_MODE =
|
|
10
|
+
export const AUTH_MODE = ensurePerceptaAuthModeEnv({
|
|
11
|
+
defaultAuthMode: APP_AUTH_MODE,
|
|
12
|
+
});
|
|
@@ -1,53 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
1
|
+
import { createTransactionalDatabaseServiceFactory } from "@percepta/database";
|
|
3
2
|
import { db } from "../drizzle/db";
|
|
4
3
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
public static create(): DatabaseService {
|
|
8
|
-
if (DatabaseService.SINGLETON == null) {
|
|
9
|
-
DatabaseService.SINGLETON = new DatabaseService(db);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return DatabaseService.SINGLETON;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
private transactionAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
|
|
16
|
-
|
|
17
|
-
private constructor(private database: NodePgDatabase) {}
|
|
18
|
-
|
|
19
|
-
public async createTransaction<TReturn>(
|
|
20
|
-
callback: (txn: NodePgDatabase) => Promise<TReturn>,
|
|
21
|
-
): Promise<TReturn> {
|
|
22
|
-
const currentContext = this.transactionAsyncLocalStorage.getStore();
|
|
23
|
-
if (currentContext != null) {
|
|
24
|
-
const { txn } = currentContext;
|
|
25
|
-
|
|
26
|
-
// Already in a transaction.
|
|
27
|
-
return callback(txn);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return this.database.transaction((txn) => {
|
|
31
|
-
return this.transactionAsyncLocalStorage.run<Promise<TReturn>>(
|
|
32
|
-
{ txn },
|
|
33
|
-
() => callback(txn),
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
public getDatabase(): Database {
|
|
39
|
-
const context = this.transactionAsyncLocalStorage.getStore();
|
|
40
|
-
if (context == null) {
|
|
41
|
-
return this.database;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const { txn } = context;
|
|
45
|
-
return txn;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type Database = Omit<NodePgDatabase, "transaction">;
|
|
50
|
-
|
|
51
|
-
interface LocalStorage {
|
|
52
|
-
txn: NodePgDatabase;
|
|
53
|
-
}
|
|
4
|
+
export const DatabaseService = createTransactionalDatabaseServiceFactory(db);
|
|
5
|
+
export type DatabaseService = ReturnType<typeof DatabaseService.create>;
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createFaroInstance } from "@percepta/next-utils/faro";
|
|
3
|
+
import { createPerceptaFaroInstance } from "@percepta/next-utils/faro";
|
|
5
4
|
import { getClientEnvConfig } from "../../config/clientEnvConfig";
|
|
6
5
|
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
FARO_APP_ENVIRONMENT,
|
|
12
|
-
} = getClientEnvConfig();
|
|
13
|
-
|
|
14
|
-
export const faroInstance = createFaroInstance({
|
|
15
|
-
collectorUrl: FARO_COLLECTOR_URL,
|
|
16
|
-
app: {
|
|
17
|
-
name: FARO_APP_NAME,
|
|
18
|
-
version: FARO_APP_VERSION,
|
|
19
|
-
environment: FARO_APP_ENVIRONMENT,
|
|
20
|
-
},
|
|
21
|
-
extraInstrumentations: [new TracingInstrumentation()],
|
|
6
|
+
export const faroInstance = createPerceptaFaroInstance({
|
|
7
|
+
defaultAppName: "__APP_NAME__",
|
|
8
|
+
defaultEnvironment: process.env.NODE_ENV,
|
|
9
|
+
env: getClientEnvConfig(),
|
|
22
10
|
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const LOCAL_POSTGRES_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
9
|
-
const LOCAL_POSTGRES_PORT = "5434";
|
|
10
|
-
const POSTGRES_SERVICE = "postgres";
|
|
11
|
-
const POSTGRES_USER = "postgres";
|
|
12
|
-
const ROOT_DIR = path.resolve(
|
|
13
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
14
|
-
"..",
|
|
15
|
-
);
|
|
16
|
-
const PACKAGES_DIR = path.join(ROOT_DIR, "packages");
|
|
17
|
-
|
|
18
|
-
const databases = new Set(["auth"]);
|
|
19
|
-
|
|
20
|
-
for (const packageDir of listPackageDirs()) {
|
|
21
|
-
const database = readPackageDatabaseName(packageDir);
|
|
22
|
-
if (database != null) {
|
|
23
|
-
databases.add(database);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
for (const database of [...databases].sort()) {
|
|
28
|
-
ensureDatabase(database);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function listPackageDirs() {
|
|
32
|
-
if (!existsSync(PACKAGES_DIR)) return [];
|
|
33
|
-
|
|
34
|
-
return readdirSync(PACKAGES_DIR, { withFileTypes: true })
|
|
35
|
-
.filter((entry) => entry.isDirectory())
|
|
36
|
-
.map((entry) => packageDirFor(entry.name));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function packageDirFor(packageName) {
|
|
40
|
-
if (
|
|
41
|
-
packageName === "." ||
|
|
42
|
-
packageName === ".." ||
|
|
43
|
-
packageName.includes("/") ||
|
|
44
|
-
packageName.includes("\\")
|
|
45
|
-
) {
|
|
46
|
-
throw new Error(`Unexpected package directory name: ${packageName}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return path.join(PACKAGES_DIR, packageName);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function readPackageDatabaseName(packageDir) {
|
|
53
|
-
const env = readPackageEnvFile(packageDir);
|
|
54
|
-
const databaseUrl = env.DATABASE_URL;
|
|
55
|
-
if (databaseUrl == null || databaseUrl.length === 0) return null;
|
|
56
|
-
|
|
57
|
-
let url;
|
|
58
|
-
try {
|
|
59
|
-
url = new URL(databaseUrl);
|
|
60
|
-
} catch {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Invalid DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must use postgres or postgresql.`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const port = url.port || "5432";
|
|
73
|
-
if (!LOCAL_POSTGRES_HOSTS.has(url.hostname) || port !== LOCAL_POSTGRES_PORT) {
|
|
74
|
-
console.log(
|
|
75
|
-
`Skipping non-local app database for ${path.relative(ROOT_DIR, packageDir)}.`,
|
|
76
|
-
);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const database = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
81
|
-
if (database.length === 0) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must include a database name.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return database;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function readPackageEnvFile(packageDir) {
|
|
91
|
-
const safePackageDir = assertPackageDir(packageDir);
|
|
92
|
-
const envPath = path.join(safePackageDir, ".env.local");
|
|
93
|
-
if (!existsSync(envPath)) return {};
|
|
94
|
-
|
|
95
|
-
const env = {};
|
|
96
|
-
const content = readFileSync(envPath, "utf8");
|
|
97
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
98
|
-
const line = rawLine.trim();
|
|
99
|
-
if (!line || line.startsWith("#")) continue;
|
|
100
|
-
|
|
101
|
-
const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
102
|
-
const separatorIndex = normalized.indexOf("=");
|
|
103
|
-
if (separatorIndex === -1) continue;
|
|
104
|
-
|
|
105
|
-
const key = normalized.slice(0, separatorIndex).trim();
|
|
106
|
-
const rawValue = normalized.slice(separatorIndex + 1).trim();
|
|
107
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
108
|
-
|
|
109
|
-
env[key] = unquote(rawValue);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return env;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function assertPackageDir(packageDir) {
|
|
116
|
-
const resolvedPackageDir = path.resolve(packageDir);
|
|
117
|
-
const relative = path.relative(PACKAGES_DIR, resolvedPackageDir);
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
relative.length === 0 ||
|
|
121
|
-
relative.startsWith("..") ||
|
|
122
|
-
path.isAbsolute(relative) ||
|
|
123
|
-
relative.includes(path.sep)
|
|
124
|
-
) {
|
|
125
|
-
throw new Error(`Unexpected package directory: ${packageDir}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return resolvedPackageDir;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function unquote(value) {
|
|
132
|
-
if (
|
|
133
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
134
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
135
|
-
) {
|
|
136
|
-
return value.slice(1, -1);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return value;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function ensureDatabase(database) {
|
|
143
|
-
const exists = execDockerCompose([
|
|
144
|
-
"exec",
|
|
145
|
-
"-T",
|
|
146
|
-
POSTGRES_SERVICE,
|
|
147
|
-
"psql",
|
|
148
|
-
"-U",
|
|
149
|
-
POSTGRES_USER,
|
|
150
|
-
"-d",
|
|
151
|
-
"postgres",
|
|
152
|
-
"-tAc",
|
|
153
|
-
`SELECT 1 FROM pg_database WHERE datname = ${quotePgLiteral(database)}`,
|
|
154
|
-
]).trim();
|
|
155
|
-
|
|
156
|
-
if (exists === "1") {
|
|
157
|
-
console.log(`Database ${database} already exists.`);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
execDockerCompose([
|
|
162
|
-
"exec",
|
|
163
|
-
"-T",
|
|
164
|
-
POSTGRES_SERVICE,
|
|
165
|
-
"createdb",
|
|
166
|
-
"-U",
|
|
167
|
-
POSTGRES_USER,
|
|
168
|
-
database,
|
|
169
|
-
]);
|
|
170
|
-
console.log(`Database ${database} created.`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function execDockerCompose(args) {
|
|
174
|
-
return execFileSync("docker", ["compose", ...args], {
|
|
175
|
-
cwd: ROOT_DIR,
|
|
176
|
-
encoding: "utf8",
|
|
177
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function quotePgLiteral(value) {
|
|
182
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
183
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { customType } from "drizzle-orm/pg-core";
|
|
2
|
-
import type { z } from "zod";
|
|
3
|
-
|
|
4
|
-
export function jsonbFromZod<TValue>(schema: z.Schema<TValue>): ReturnType<
|
|
5
|
-
typeof customType<{
|
|
6
|
-
data: TValue;
|
|
7
|
-
driverData: string;
|
|
8
|
-
}>
|
|
9
|
-
> {
|
|
10
|
-
return customType<{
|
|
11
|
-
data: TValue;
|
|
12
|
-
driverData: string;
|
|
13
|
-
}>({
|
|
14
|
-
dataType() {
|
|
15
|
-
return "jsonb";
|
|
16
|
-
},
|
|
17
|
-
toDriver(value) {
|
|
18
|
-
return JSON.stringify(schema.parse(value));
|
|
19
|
-
},
|
|
20
|
-
fromDriver(raw) {
|
|
21
|
-
const parsed = schema.parse(raw);
|
|
22
|
-
return parsed;
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}
|