aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Dockerfile53
-rw-r--r--package-lock.json83
-rw-r--r--package.json4
-rwxr-xr-xscripts/build-image.sh3
-rw-r--r--scripts/build.env22
-rwxr-xr-xscripts/container-entrypoint.sh5
-rw-r--r--src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx2
-rw-r--r--src/app/groups/[groupId]/expenses/create/page.tsx2
-rw-r--r--src/components/expense-form.tsx7
-rw-r--r--src/lib/env.ts20
-rw-r--r--src/lib/featureFlags.ts15
12 files changed, 143 insertions, 74 deletions
diff --git a/.gitignore b/.gitignore
index 8d8a4fc..9d718ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ yarn-error.log*
# local env files
.env*.local
*.env
+!scripts/build.env
# vercel
.vercel
diff --git a/Dockerfile b/Dockerfile
index d31173b..3fefd3c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,47 @@
-FROM node:21-slim as base
+FROM node:21-alpine as base
-EXPOSE 3000/tcp
WORKDIR /usr/app
-COPY ./ ./
+COPY ./package.json \
+ ./package-lock.json \
+ ./next.config.js \
+ ./tsconfig.json \
+ ./reset.d.ts \
+ ./tailwind.config.js \
+ ./postcss.config.js ./
+COPY ./scripts ./scripts
+COPY ./prisma ./prisma
+COPY ./src ./src
-RUN apt update && \
- apt install openssl -y && \
- apt clean && \
- apt autoclean && \
- apt autoremove && \
+RUN apk add --no-cache openssl && \
npm ci --ignore-scripts && \
- npm install -g prisma && \
- prisma generate
+ npx prisma generate
-# env vars needed for build not to fail
-ARG POSTGRES_PRISMA_URL
-ARG POSTGRES_URL_NON_POOLING
+ENV NEXT_TELEMETRY_DISABLED=1
+COPY scripts/build.env .env
RUN npm run build
-ENTRYPOINT ["/usr/app/scripts/container-entrypoint.sh"]
+RUN rm -r .next/cache
+
+FROM node:21-alpine as runtime-deps
+
+WORKDIR /usr/app
+COPY --from=base /usr/app/package.json /usr/app/package-lock.json ./
+COPY --from=base /usr/app/prisma ./prisma
+
+RUN npm ci --omit=dev --omit=optional --ignore-scripts && \
+ npx prisma generate
+
+FROM node:21-alpine as runner
+
+EXPOSE 3000/tcp
+WORKDIR /usr/app
+
+COPY --from=base /usr/app/package.json /usr/app/package-lock.json ./
+COPY --from=runtime-deps /usr/app/node_modules ./node_modules
+COPY ./public ./public
+COPY ./scripts ./scripts
+COPY --from=base /usr/app/prisma ./prisma
+COPY --from=base /usr/app/.next ./.next
+
+ENTRYPOINT ["/bin/sh", "/usr/app/scripts/container-entrypoint.sh"]
diff --git a/package-lock.json b/package-lock.json
index 9a55903..52b0e54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
"next13-progressbar": "^1.1.1",
"openai": "^4.25.0",
"pg": "^8.11.3",
+ "prisma": "^5.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
@@ -65,7 +66,6 @@
"postcss": "^8",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
- "prisma": "^5.7.0",
"tailwindcss": "^3",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
@@ -1429,13 +1429,10 @@
}
},
"node_modules/@prisma/client": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz",
- "integrity": "sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==",
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.9.1.tgz",
+ "integrity": "sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==",
"hasInstallScript": true,
- "dependencies": {
- "@prisma/engines-version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee"
- },
"engines": {
"node": ">=16.13"
},
@@ -1449,59 +1446,48 @@
}
},
"node_modules/@prisma/debug": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz",
- "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==",
- "devOptional": true
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.9.1.tgz",
+ "integrity": "sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA=="
},
"node_modules/@prisma/engines": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz",
- "integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==",
- "devOptional": true,
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.9.1.tgz",
+ "integrity": "sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==",
"hasInstallScript": true,
"dependencies": {
- "@prisma/debug": "5.7.0",
- "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
- "@prisma/fetch-engine": "5.7.0",
- "@prisma/get-platform": "5.7.0"
+ "@prisma/debug": "5.9.1",
+ "@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
+ "@prisma/fetch-engine": "5.9.1",
+ "@prisma/get-platform": "5.9.1"
}
},
- "node_modules/@prisma/engines-version": {
- "version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz",
- "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw=="
- },
"node_modules/@prisma/engines/node_modules/@prisma/engines-version": {
- "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz",
- "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==",
- "devOptional": true
+ "version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz",
+ "integrity": "sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ=="
},
"node_modules/@prisma/fetch-engine": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz",
- "integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==",
- "devOptional": true,
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz",
+ "integrity": "sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==",
"dependencies": {
- "@prisma/debug": "5.7.0",
- "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
- "@prisma/get-platform": "5.7.0"
+ "@prisma/debug": "5.9.1",
+ "@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
+ "@prisma/get-platform": "5.9.1"
}
},
"node_modules/@prisma/fetch-engine/node_modules/@prisma/engines-version": {
- "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz",
- "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==",
- "devOptional": true
+ "version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz",
+ "integrity": "sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ=="
},
"node_modules/@prisma/get-platform": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz",
- "integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==",
- "devOptional": true,
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.9.1.tgz",
+ "integrity": "sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==",
"dependencies": {
- "@prisma/debug": "5.7.0"
+ "@prisma/debug": "5.9.1"
}
},
"node_modules/@radix-ui/number": {
@@ -7430,13 +7416,12 @@
}
},
"node_modules/prisma": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz",
- "integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==",
- "devOptional": true,
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.9.1.tgz",
+ "integrity": "sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==",
"hasInstallScript": true,
"dependencies": {
- "@prisma/engines": "5.7.0"
+ "@prisma/engines": "5.9.1"
},
"bin": {
"prisma": "build/index.js"
diff --git a/package.json b/package.json
index 1451f5a..5706b1e 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,8 @@
"ts-pattern": "^5.0.6",
"uuid": "^9.0.1",
"vaul": "^0.8.0",
- "zod": "^3.22.4"
+ "zod": "^3.22.4",
+ "prisma": "^5.7.0"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
@@ -70,7 +71,6 @@
"postcss": "^8",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
- "prisma": "^5.7.0",
"tailwindcss": "^3",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
diff --git a/scripts/build-image.sh b/scripts/build-image.sh
index 95b8acb..c900089 100755
--- a/scripts/build-image.sh
+++ b/scripts/build-image.sh
@@ -5,9 +5,6 @@ SPLIIT_VERSION=$(node -p -e "require('./package.json').version")
# we need to set dummy data for POSTGRES env vars in order for build not to fail
docker buildx build \
- --no-cache \
- --build-arg POSTGRES_PRISMA_URL=postgresql://build:@db \
- --build-arg POSTGRES_URL_NON_POOLING=postgresql://build:@db \
-t ${SPLIIT_APP_NAME}:${SPLIIT_VERSION} \
-t ${SPLIIT_APP_NAME}:latest \
.
diff --git a/scripts/build.env b/scripts/build.env
new file mode 100644
index 0000000..98b3fac
--- /dev/null
+++ b/scripts/build.env
@@ -0,0 +1,22 @@
+# build file that contains all possible env vars with mocked values
+# as most of them are used at build time in order to have the production build to work properly
+
+# db
+POSTGRES_PASSWORD=1234
+
+# app
+POSTGRES_PRISMA_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db
+POSTGRES_URL_NON_POOLING=postgresql://postgres:${POSTGRES_PASSWORD}@db
+
+# app-minio
+NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS=false
+S3_UPLOAD_KEY=AAAAAAAAAAAAAAAAAAAA
+S3_UPLOAD_SECRET=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+S3_UPLOAD_BUCKET=spliit
+S3_UPLOAD_REGION=eu-north-1
+S3_UPLOAD_ENDPOINT=s3://minio.example.com
+
+# app-openai
+NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT=false
+OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
+NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT=false
diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh
index 364787e..5871314 100755
--- a/scripts/container-entrypoint.sh
+++ b/scripts/container-entrypoint.sh
@@ -1,3 +1,6 @@
#!/bin/bash
-prisma migrate deploy
+
+set -euxo pipefail
+
+npx prisma migrate deploy
npm run start
diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
index 65b2b50..2514a77 100644
--- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
@@ -6,6 +6,7 @@ import {
getExpense,
updateExpense,
} from '@/lib/api'
+import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
import { expenseFormSchema } from '@/lib/schemas'
import { Metadata } from 'next'
import { notFound, redirect } from 'next/navigation'
@@ -47,6 +48,7 @@ export default async function EditExpensePage({
categories={categories}
onSubmit={updateExpenseAction}
onDelete={deleteExpenseAction}
+ runtimeFeatureFlags={await getRuntimeFeatureFlags()}
/>
</Suspense>
)
diff --git a/src/app/groups/[groupId]/expenses/create/page.tsx b/src/app/groups/[groupId]/expenses/create/page.tsx
index 2d580d3..6398fd1 100644
--- a/src/app/groups/[groupId]/expenses/create/page.tsx
+++ b/src/app/groups/[groupId]/expenses/create/page.tsx
@@ -1,6 +1,7 @@
import { cached } from '@/app/cached-functions'
import { ExpenseForm } from '@/components/expense-form'
import { createExpense, getCategories } from '@/lib/api'
+import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
import { expenseFormSchema } from '@/lib/schemas'
import { Metadata } from 'next'
import { notFound, redirect } from 'next/navigation'
@@ -32,6 +33,7 @@ export default async function ExpensePage({
group={group}
categories={categories}
onSubmit={createExpenseAction}
+ runtimeFeatureFlags={await getRuntimeFeatureFlags()}
/>
</Suspense>
)
diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx
index fd3f6e2..a7e549c 100644
--- a/src/components/expense-form.tsx
+++ b/src/components/expense-form.tsx
@@ -35,6 +35,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
+import { RuntimeFeatureFlags } from '@/lib/featureFlags'
import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
@@ -52,6 +53,7 @@ export type Props = {
categories: NonNullable<Awaited<ReturnType<typeof getCategories>>>
onSubmit: (values: ExpenseFormValues) => Promise<void>
onDelete?: () => Promise<void>
+ runtimeFeatureFlags: RuntimeFeatureFlags
}
export function ExpenseForm({
@@ -60,6 +62,7 @@ export function ExpenseForm({
categories,
onSubmit,
onDelete,
+ runtimeFeatureFlags,
}: Props) {
const isCreate = expense === undefined
const searchParams = useSearchParams()
@@ -161,7 +164,7 @@ export function ExpenseForm({
{...field}
onBlur={async () => {
field.onBlur() // avoid skipping other blur event listeners since we overwrite `field`
- if (process.env.NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT) {
+ if (runtimeFeatureFlags.enableCategoryExtract) {
setCategoryLoading(true)
const { categoryId } = await extractCategoryFromTitle(
field.value,
@@ -541,7 +544,7 @@ export function ExpenseForm({
</CardContent>
</Card>
- {process.env.NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS && (
+ {runtimeFeatureFlags.enableExpenseDocuments && (
<Card className="mt-4">
<CardHeader>
<CardTitle className="flex justify-between">
diff --git a/src/lib/env.ts b/src/lib/env.ts
index 927e756..920df14 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -1,5 +1,10 @@
import { ZodIssueCode, z } from 'zod'
+const interpretEnvVarAsBool = (val: unknown): boolean => {
+ if (typeof val !== 'string') return false
+ return ['true', 'yes', '1', 'on'].includes(val.toLowerCase())
+}
+
const envSchema = z
.object({
POSTGRES_URL_NON_POOLING: z.string().url(),
@@ -12,14 +17,23 @@ const envSchema = z
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000',
),
- NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS: z.coerce.boolean().default(false),
+ NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS: z.preprocess(
+ interpretEnvVarAsBool,
+ z.boolean().default(false),
+ ),
S3_UPLOAD_KEY: z.string().optional(),
S3_UPLOAD_SECRET: z.string().optional(),
S3_UPLOAD_BUCKET: z.string().optional(),
S3_UPLOAD_REGION: z.string().optional(),
S3_UPLOAD_ENDPOINT: z.string().optional(),
- NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT: z.coerce.boolean().default(false),
- NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT: z.coerce.boolean().default(false),
+ NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT: z.preprocess(
+ interpretEnvVarAsBool,
+ z.boolean().default(false),
+ ),
+ NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT: z.preprocess(
+ interpretEnvVarAsBool,
+ z.boolean().default(false),
+ ),
OPENAI_API_KEY: z.string().optional(),
})
.superRefine((env, ctx) => {
diff --git a/src/lib/featureFlags.ts b/src/lib/featureFlags.ts
new file mode 100644
index 0000000..e57ca9c
--- /dev/null
+++ b/src/lib/featureFlags.ts
@@ -0,0 +1,15 @@
+'use server'
+
+import { env } from './env'
+
+export async function getRuntimeFeatureFlags() {
+ return {
+ enableExpenseDocuments: env.NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS,
+ enableReceiptExtract: env.NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT,
+ enableCategoryExtract: env.NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT,
+ }
+}
+
+export type RuntimeFeatureFlags = Awaited<
+ ReturnType<typeof getRuntimeFeatureFlags>
+>