From 575c4cbedb8ee4039691ed2b3120bd64a6e45b3b Mon Sep 17 00:00:00 2001 From: Jozef Dochan <51478129+dochan-consultis@users.noreply.github.com> Date: Thu, 22 May 2025 15:08:13 +0200 Subject: [PATCH] make deployment easier (#225) --- Dockerfile | 74 ++++++++++++++++++ backend/Dockerfile | 11 --- backend/config/config.json | 2 +- backend/index.js | 152 +------------------------------------ backend/package.json | 6 +- backend/scripts/drop.sh | 4 - backend/scripts/migrate.sh | 4 - backend/scripts/seed.sh | 3 - backend/server.js | 143 ++++++++++++++++++++++++++++++++++ combined_server.js | 87 +++++++++++++++++++++ docker-compose.yaml | 27 ++----- frontend/Dockerfile | 64 ---------------- frontend/config/config.ts | 2 +- 13 files changed, 318 insertions(+), 261 deletions(-) create mode 100644 Dockerfile delete mode 100644 backend/Dockerfile delete mode 100644 backend/scripts/drop.sh delete mode 100644 backend/scripts/migrate.sh delete mode 100644 backend/scripts/seed.sh create mode 100644 backend/server.js create mode 100644 combined_server.js delete mode 100644 frontend/Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..84fbc81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,74 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install root dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Install frontend dependencies +WORKDIR /app/frontend +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm ci + +# Install backend dependencies +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json* ./ +RUN npm ci + +# Build frontend +FROM base AS frontend-builder +WORKDIR /app + +COPY package.json ./ +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/frontend/node_modules ./frontend/node_modules +COPY frontend ./frontend + +WORKDIR /app/frontend +RUN npm run build + +# Final production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=8000 +ENV FRONTEND_ORIGIN=http://localhost:8000 + +# Create a non-root user +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy backend files +COPY backend ./backend + +# Copy frontend build +COPY --from=frontend-builder /app/frontend/.next/standalone ./ +COPY --from=frontend-builder /app/frontend/.next/static ./.next/static +COPY --from=frontend-builder /app/frontend/public ./public + +# Copy Next.js module for the server +COPY --from=deps /app/frontend/node_modules/next ./node_modules/next + +# Install backend dependencies directly in the container to ensure native modules are built correctly +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json* ./ +RUN npm ci +WORKDIR /app + +# Copy custom combined_server.js that combines frontend and backend +COPY combined_server.js ./ + +# Switch to non-root user +USER nextjs + +# Expose the port +EXPOSE 8000 + +# Run database migrations and start the combined server +CMD ["node", "combined_server.js"] diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 9484f1e..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci -COPY . . - -EXPOSE $PORT -RUN npx sequelize db:migrate - -CMD ["node", "index.js"] diff --git a/backend/config/config.json b/backend/config/config.json index 32f9227..339460d 100644 --- a/backend/config/config.json +++ b/backend/config/config.json @@ -9,6 +9,6 @@ }, "production": { "dialect": "sqlite", - "storage": "database/database.sqlite" + "storage": "../database/database.sqlite" } } diff --git a/backend/index.js b/backend/index.js index 245d9b7..13a5f6b 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,153 +1,7 @@ -const express = require('express'); -const RateLimit = require('express-rate-limit'); -const path = require('path'); -const { Sequelize } = require('sequelize'); -const app = express(); - -// enable frontend access -const cors = require('cors'); -const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:8000'; -const corsOptions = { - origin: frontendOrigin, - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', -}; -app.use(cors(corsOptions)); - -// enable json middleware -app.use(express.json()); - -// enable rate limiter -const limiter = RateLimit({ - windowMs: 60 * 60 * 1000, // 1h - max: 1000, // 1000 requests per hour - message: 'Too many requests from this IP, please try again after an hour', -}); -app.use(limiter); - -// Specify the directory to serve static files -app.use(express.static(path.join(__dirname, 'public'))); - -// init sequalize -const sequelize = new Sequelize({ - dialect: 'sqlite', - storage: 'database/database.sqlite', - logging: false, -}); - -// "/" -const indexRoute = require('./routes/index'); -app.use('/', indexRoute); - -// "users" -const usersIndexRoute = require('./routes/users/index')(sequelize); -const usersFindRoute = require('./routes/users/find')(sequelize); -const usersSearchRoute = require('./routes/users/search')(sequelize); -const signUpRoute = require('./routes/users/signup')(sequelize); -const signInRoute = require('./routes/users/signin')(sequelize); -app.use('/users', usersIndexRoute); -app.use('/users', usersFindRoute); -app.use('/users', usersSearchRoute); -app.use('/users', signUpRoute); -app.use('/users', signInRoute); -// ESM import -(async () => { - const updateRoute = await import('./routes/users/update.mjs'); - app.use('/users', updateRoute.default(sequelize)); -})(); - -// "/projects" -const projectsIndexRoute = require('./routes/projects/index')(sequelize); -const projectsShowRoute = require('./routes/projects/show')(sequelize); -const projectsNewRoute = require('./routes/projects/new')(sequelize); -const projectsEditRoute = require('./routes/projects/edit')(sequelize); -const projectsDeleteRoute = require('./routes/projects/delete')(sequelize); -app.use('/projects', projectsIndexRoute); -app.use('/projects', projectsShowRoute); -app.use('/projects', projectsNewRoute); -app.use('/projects', projectsEditRoute); -app.use('/projects', projectsDeleteRoute); - -// "/folders" -const foldersIndexRoute = require('./routes/folders/index')(sequelize); -const foldersNewRoute = require('./routes/folders/new')(sequelize); -const foldersEditRoute = require('./routes/folders/edit')(sequelize); -const foldersDeleteRoute = require('./routes/folders/delete')(sequelize); -app.use('/folders', foldersIndexRoute); -app.use('/folders', foldersNewRoute); -app.use('/folders', foldersEditRoute); -app.use('/folders', foldersDeleteRoute); - -// "/cases" -const casesDownloadRoute = require('./routes/cases/download')(sequelize); -const casesIndexRoute = require('./routes/cases/index')(sequelize); -const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize); -const casesShowRoute = require('./routes/cases/show')(sequelize); -const casesNewRoute = require('./routes/cases/new')(sequelize); -const casesEditRoute = require('./routes/cases/edit')(sequelize); -const casesDeleteRoute = require('./routes/cases/delete')(sequelize); -app.use('/cases', casesDownloadRoute); -app.use('/cases', casesIndexRoute); -app.use('/cases', casesIndexByProjectIdRoute); -app.use('/cases', casesShowRoute); -app.use('/cases', casesNewRoute); -app.use('/cases', casesEditRoute); -app.use('/cases', casesDeleteRoute); - -// "/steps" -const stepsEditRoute = require('./routes/steps/edit')(sequelize); -app.use('/steps', stepsEditRoute); - -// "/attachments" -const attachmentsNewRoute = require('./routes/attachments/new')(sequelize); -const attachmentsDeleteRoute = require('./routes/attachments/delete')(sequelize); -const attachmentsDownloadRoute = require('./routes/attachments/download')(sequelize); -app.use('/attachments', attachmentsNewRoute); -app.use('/attachments', attachmentsDeleteRoute); -app.use('/attachments', attachmentsDownloadRoute); - -// "/runs" -const runsDownloadRoute = require('./routes/runs/download')(sequelize); -const runsIndexRoute = require('./routes/runs/index')(sequelize); -const runsShowRoute = require('./routes/runs/show')(sequelize); -const runsNewRoute = require('./routes/runs/new')(sequelize); -const runsEditRoute = require('./routes/runs/edit')(sequelize); -const runDeleteRoute = require('./routes/runs/delete')(sequelize); -app.use('/runs', runsDownloadRoute); -app.use('/runs', runsIndexRoute); -app.use('/runs', runsShowRoute); -app.use('/runs', runsNewRoute); -app.use('/runs', runsEditRoute); -app.use('/runs', runDeleteRoute); - -// "/runcases" -const runCaseIndexRoute = require('./routes/runcases/index')(sequelize); -const runCaseEditRoute = require('./routes/runcases/edit')(sequelize); -app.use('/runcases', runCaseIndexRoute); -app.use('/runcases', runCaseEditRoute); - -// "/members" -const membersIndexRoute = require('./routes/members/index')(sequelize); -const membersNewRoute = require('./routes/members/new')(sequelize); -const membersEditRoute = require('./routes/members/edit')(sequelize); -const membersDeleteRoute = require('./routes/members/delete')(sequelize); -const membersCheckRoute = require('./routes/members/check')(sequelize); -app.use('/members', membersIndexRoute); -app.use('/members', membersNewRoute); -app.use('/members', membersEditRoute); -app.use('/members', membersDeleteRoute); -app.use('/members', membersCheckRoute); - -// "/home" -const homeIndexRoute = require('./routes/home/index')(sequelize); -app.use('/home', homeIndexRoute); +const server = require('./server'); const PORT = process.env.PORT || 8001; -app.listen(PORT, () => { + +server.listen(PORT, () => { console.log(`Backend server is running on port ${PORT}`); - console.log(`Access from the frontend origin: ${frontendOrigin} is valid.`); - if (!process.env.SECRET_KEY) { - console.log( - "[Warning]: Default key is used for token generation. Please set the environment variable 'SECRET_KEY'`." - ); - } }); diff --git a/backend/package.json b/backend/package.json index b2b5881..60980e0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,9 +5,9 @@ "scripts": { "start": "node --env-file=.env index", "dev": "node index", - "migrate": "bash scripts/migrate.sh", - "seed": "bash scripts/seed.sh", - "drop": "bash scripts/drop.sh" + "migrate": "sequelize db:migrate", + "seed": "sequelize db:seed:all", + "drop": "sequelize db:migrate:undo:all" }, "author": "", "license": "ISC", diff --git a/backend/scripts/drop.sh b/backend/scripts/drop.sh deleted file mode 100644 index 212267c..0000000 --- a/backend/scripts/drop.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -npx sequelize db:migrate:undo:all -echo "Dropped all tables" diff --git a/backend/scripts/migrate.sh b/backend/scripts/migrate.sh deleted file mode 100644 index e581f3f..0000000 --- a/backend/scripts/migrate.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -npx sequelize db:migrate -echo "Migration complete." diff --git a/backend/scripts/seed.sh b/backend/scripts/seed.sh deleted file mode 100644 index fc492ff..0000000 --- a/backend/scripts/seed.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -npx sequelize db:seed:all diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..cedb785 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,143 @@ +const express = require('express'); +const RateLimit = require('express-rate-limit'); +const path = require('path'); +const { Sequelize } = require('sequelize'); +const app = express(); + +// enable json middleware +app.use(express.json()); + +// enable rate limiter +const limiter = RateLimit({ + windowMs: 60 * 60 * 1000, // 1h + max: 1000, // 1000 requests per hour + message: 'Too many requests from this IP, please try again after an hour', +}); +app.use(limiter); + +// Specify the directory to serve static files +app.use(express.static(path.join(__dirname, 'public'))); + +// init sequalize +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'database/database.sqlite', + logging: false, +}); + +// "/" +const indexRoute = require('./routes/index'); +app.use('/', indexRoute); + +// "users" +const usersIndexRoute = require('./routes/users/index')(sequelize); +const usersFindRoute = require('./routes/users/find')(sequelize); +const usersSearchRoute = require('./routes/users/search')(sequelize); +const signUpRoute = require('./routes/users/signup')(sequelize); +const signInRoute = require('./routes/users/signin')(sequelize); +app.use('/users', usersIndexRoute); +app.use('/users', usersFindRoute); +app.use('/users', usersSearchRoute); +app.use('/users', signUpRoute); +app.use('/users', signInRoute); +// ESM import +(async () => { + const updateRoute = await import('./routes/users/update.mjs'); + app.use('/users', updateRoute.default(sequelize)); +})(); + +// "/projects" +const projectsIndexRoute = require('./routes/projects/index')(sequelize); +const projectsShowRoute = require('./routes/projects/show')(sequelize); +const projectsNewRoute = require('./routes/projects/new')(sequelize); +const projectsEditRoute = require('./routes/projects/edit')(sequelize); +const projectsDeleteRoute = require('./routes/projects/delete')(sequelize); +app.use('/projects', projectsIndexRoute); +app.use('/projects', projectsShowRoute); +app.use('/projects', projectsNewRoute); +app.use('/projects', projectsEditRoute); +app.use('/projects', projectsDeleteRoute); + +// "/folders" +const foldersIndexRoute = require('./routes/folders/index')(sequelize); +const foldersNewRoute = require('./routes/folders/new')(sequelize); +const foldersEditRoute = require('./routes/folders/edit')(sequelize); +const foldersDeleteRoute = require('./routes/folders/delete')(sequelize); +app.use('/folders', foldersIndexRoute); +app.use('/folders', foldersNewRoute); +app.use('/folders', foldersEditRoute); +app.use('/folders', foldersDeleteRoute); + +// "/cases" +// "/cases" +const casesDownloadRoute = require('./routes/cases/download')(sequelize); +const casesIndexRoute = require('./routes/cases/index')(sequelize); +const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize); +const casesShowRoute = require('./routes/cases/show')(sequelize); +const casesNewRoute = require('./routes/cases/new')(sequelize); +const casesEditRoute = require('./routes/cases/edit')(sequelize); +const casesDeleteRoute = require('./routes/cases/delete')(sequelize); +app.use('/cases', casesDownloadRoute); +app.use('/cases', casesIndexRoute); +app.use('/cases', casesIndexByProjectIdRoute); +app.use('/cases', casesShowRoute); +app.use('/cases', casesNewRoute); +app.use('/cases', casesEditRoute); +app.use('/cases', casesDeleteRoute); + +// "/steps" +const stepsEditRoute = require('./routes/steps/edit')(sequelize); +app.use('/steps', stepsEditRoute); + +// "/attachments" +const attachmentsNewRoute = require('./routes/attachments/new')(sequelize); +const attachmentsDeleteRoute = require('./routes/attachments/delete')(sequelize); +const attachmentsDownloadRoute = require('./routes/attachments/download')(sequelize); +app.use('/attachments', attachmentsNewRoute); +app.use('/attachments', attachmentsDeleteRoute); +app.use('/attachments', attachmentsDownloadRoute); + +// "/runs" +const runsDownloadRoute = require('./routes/runs/download')(sequelize); +const runsIndexRoute = require('./routes/runs/index')(sequelize); +const runsShowRoute = require('./routes/runs/show')(sequelize); +const runsNewRoute = require('./routes/runs/new')(sequelize); +const runsEditRoute = require('./routes/runs/edit')(sequelize); +const runDeleteRoute = require('./routes/runs/delete')(sequelize); +app.use('/runs', runsDownloadRoute); +app.use('/runs', runsIndexRoute); +app.use('/runs', runsShowRoute); +app.use('/runs', runsNewRoute); +app.use('/runs', runsEditRoute); +app.use('/runs', runDeleteRoute); + +// "/runcases" +const runCaseIndexRoute = require('./routes/runcases/index')(sequelize); +const runCaseEditRoute = require('./routes/runcases/edit')(sequelize); +app.use('/runcases', runCaseIndexRoute); +app.use('/runcases', runCaseEditRoute); + +// "/members" +const membersIndexRoute = require('./routes/members/index')(sequelize); +const membersNewRoute = require('./routes/members/new')(sequelize); +const membersEditRoute = require('./routes/members/edit')(sequelize); +const membersDeleteRoute = require('./routes/members/delete')(sequelize); +const membersCheckRoute = require('./routes/members/check')(sequelize); +app.use('/members', membersIndexRoute); +app.use('/members', membersNewRoute); +app.use('/members', membersEditRoute); +app.use('/members', membersDeleteRoute); +app.use('/members', membersCheckRoute); + +// "/home" +const homeIndexRoute = require('./routes/home/index')(sequelize); +app.use('/home', homeIndexRoute); + +if (!process.env.SECRET_KEY) { + console.log( + "[Warning]: Default key is used for token generation. Please set the environment variable 'SECRET_KEY'`." + ); +} + +// Export the app instead of starting the server +module.exports = app; diff --git a/combined_server.js b/combined_server.js new file mode 100644 index 0000000..ea552a4 --- /dev/null +++ b/combined_server.js @@ -0,0 +1,87 @@ +// Use express from backend node_modules +const express = require('./backend/node_modules/express'); +const { createServer: createHttpServer } = require('http'); +const path = require('path'); +const fs = require('fs'); +const { execSync } = require('child_process'); + +async function runMigrations() { + try { + console.log('Running database migrations...'); + // Use execSync with cwd option to run in the backend directory + execSync('npx sequelize-cli db:migrate', { + cwd: path.join(__dirname, 'backend'), + stdio: 'inherit', + }); + + console.log('Database migrations completed successfully.'); + + if (process.env.IS_DEMO === 'true' || process.env.IS_DEMO === '1') { + console.log('Demo mode detected. Seeding the database...'); + execSync('npx sequelize-cli db:seed:all', { + cwd: path.join(__dirname, 'backend'), + stdio: 'inherit', + }); + console.log('Database seeding completed successfully.'); + } + } catch (error) { + console.error('Error running database migrations or seeding:', error); + throw error; + } +} + +function startServer() { + try { + const server = express(); + const httpServer = createHttpServer(server); + + // Import the backend app + const backendApp = require('./backend/server'); + + // Use the backend app for /api routes + server.use('/api', backendApp); + + // For Next.js standalone build + // Check if we have the Next.js server file + const nextServerPath = './node_modules/next/dist/server/next.js'; + if (fs.existsSync(nextServerPath)) { + // Import Next.js + const next = require(nextServerPath); + + // Initialize Next.js app + const dev = process.env.NODE_ENV !== 'production'; + const nextApp = next({ dev, dir: path.join(__dirname, '.') }); + const handle = nextApp.getRequestHandler(); + + // Prepare Next.js + nextApp.prepare().then(() => { + console.log('nextjs prepared'); + // Use Next.js to handle all other routes + server.all('*', (req, res) => { + return handle(req, res); + }); + }); + } else { + console.error('Next.js module not found at:', nextServerPath); + server.all('*', (req, res) => { + res.status(500).send('Frontend server not available'); + }); + } + + const PORT = process.env.PORT || 8000; + httpServer.listen(PORT, (err) => { + if (err) throw err; + console.log(`> Ready on http://localhost:${PORT}`); + }); + } catch (error) { + console.error('Error starting server:', error); + process.exit(1); + } +} + +runMigrations().then(() => { + startServer(); +}).catch(error => { + console.error('Failed to start application:', error); + process.exit(1); +}); diff --git a/docker-compose.yaml b/docker-compose.yaml index e27c12e..facb97f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,31 +1,16 @@ version: '3.8' services: - backend: - image: unittcms-backend:latest - build: - context: ./backend - ports: - - '8001:8001' - environment: - - NODE_ENV=production - - FRONTEND_ORIGIN=http://localhost:8000 - - PORT=8001 - volumes: - - db-data:/app/database - - frontend: - image: unittcms-frontend:latest - build: - context: ./frontend + unittcms: + image: unittcms:latest ports: - '8000:8000' environment: - - NODE_ENV=production - - NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001 - PORT=8000 - depends_on: - - backend + - SECRET_KEY=your_secret_key_here + - IS_DEMO=false # set to true to seed the database + volumes: + - db-data:/app/database volumes: db-data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 0db520c..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,64 +0,0 @@ -FROM node:20-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ - else echo "Lockfile not found." && exit 1; \ - fi - - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -# ENV NEXT_TELEMETRY_DISABLED 1 - -RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ - else echo "Lockfile not found." && exit 1; \ - fi - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -# Uncomment the following line in case you want to disable telemetry during runtime. -# ENV NEXT_TELEMETRY_DISABLED 1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 8000 - -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/next-config-js/output -CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file diff --git a/frontend/config/config.ts b/frontend/config/config.ts index 52f9692..f710065 100644 --- a/frontend/config/config.ts +++ b/frontend/config/config.ts @@ -1,5 +1,5 @@ const Config = { - apiServer: process.env.NEXT_PUBLIC_BACKEND_ORIGIN || 'http://localhost:8001', + apiServer: process.env.NEXT_PUBLIC_BACKEND_ORIGIN || '/api', // set 'NEXT_PUBLIC_IS_DEMO=true' in frontend/.env isDemoSite: process.env.NEXT_PUBLIC_IS_DEMO === 'true' || false,