@nubase/create 0.1.1
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/dist/index.d.ts +1 -0
- package/dist/index.js +148 -0
- package/package.json +55 -0
- package/templates/backend/.env.development.template +4 -0
- package/templates/backend/.env.test.template +4 -0
- package/templates/backend/db/schema.sql +95 -0
- package/templates/backend/docker/dev/docker-compose.yml +21 -0
- package/templates/backend/docker/dev/postgresql-init/dump.sql +95 -0
- package/templates/backend/docker/test/docker-compose.yml +21 -0
- package/templates/backend/docker/test/postgresql-init/dump.sql +95 -0
- package/templates/backend/drizzle.config.ts +10 -0
- package/templates/backend/package.json +47 -0
- package/templates/backend/src/api/routes/auth.ts +209 -0
- package/templates/backend/src/api/routes/index.ts +2 -0
- package/templates/backend/src/api/routes/ticket.ts +62 -0
- package/templates/backend/src/auth/index.ts +37 -0
- package/templates/backend/src/db/helpers/drizzle.ts +34 -0
- package/templates/backend/src/db/schema/index.ts +4 -0
- package/templates/backend/src/db/schema/ticket.ts +13 -0
- package/templates/backend/src/db/schema/user-workspace.ts +17 -0
- package/templates/backend/src/db/schema/user.ts +10 -0
- package/templates/backend/src/db/schema/workspace.ts +9 -0
- package/templates/backend/src/db/seed.ts +71 -0
- package/templates/backend/src/helpers/env.ts +10 -0
- package/templates/backend/src/index.ts +55 -0
- package/templates/backend/src/middleware/workspace-middleware.ts +11 -0
- package/templates/backend/tsconfig.json +15 -0
- package/templates/backend/vitest.config.ts +8 -0
- package/templates/frontend/.env.development.template +1 -0
- package/templates/frontend/index.html +13 -0
- package/templates/frontend/package.json +30 -0
- package/templates/frontend/postcss.config.js +5 -0
- package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +59 -0
- package/templates/frontend/src/config.tsx +76 -0
- package/templates/frontend/src/main.tsx +6 -0
- package/templates/frontend/src/resources/ticket.ts +32 -0
- package/templates/frontend/src/styles/theme.css +3 -0
- package/templates/frontend/src/vite-env.d.ts +9 -0
- package/templates/frontend/tsconfig.json +17 -0
- package/templates/frontend/vite.config.ts +14 -0
- package/templates/root/README.md +94 -0
- package/templates/root/biome.json.template +40 -0
- package/templates/root/package.json +30 -0
- package/templates/root/turbo.json +18 -0
- package/templates/schema/package.json +28 -0
- package/templates/schema/src/api-endpoints.ts +34 -0
- package/templates/schema/src/index.ts +3 -0
- package/templates/schema/src/schema/auth.ts +89 -0
- package/templates/schema/src/schema/ticket.ts +68 -0
- package/templates/schema/tsconfig.json +14 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { program } from "commander";
|
|
10
|
+
import fse from "fs-extra";
|
|
11
|
+
import prompts from "prompts";
|
|
12
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
var __dirname = path.dirname(__filename);
|
|
14
|
+
var TEMPLATE_DIR = path.join(__dirname, "..", "templates");
|
|
15
|
+
function toPascalCase(str) {
|
|
16
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
17
|
+
}
|
|
18
|
+
function toCamelCase(str) {
|
|
19
|
+
const pascal = toPascalCase(str);
|
|
20
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
21
|
+
}
|
|
22
|
+
function toKebabCase(str) {
|
|
23
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function replaceInContent(content, options) {
|
|
26
|
+
const kebabName = toKebabCase(options.name);
|
|
27
|
+
const pascalName = toPascalCase(options.name);
|
|
28
|
+
const camelName = toCamelCase(options.name);
|
|
29
|
+
return content.replace(/__PROJECT_NAME__/g, kebabName).replace(/__PROJECT_NAME_PASCAL__/g, pascalName).replace(/__PROJECT_NAME_CAMEL__/g, camelName).replace(/__DB_NAME__/g, options.dbName).replace(/__DB_USER__/g, options.dbUser).replace(/__DB_PASSWORD__/g, options.dbPassword).replace(/__DEV_PORT__/g, String(options.devPort)).replace(/__TEST_PORT__/g, String(options.testPort)).replace(/__BACKEND_PORT__/g, String(options.backendPort)).replace(/__FRONTEND_PORT__/g, String(options.frontendPort));
|
|
30
|
+
}
|
|
31
|
+
function copyTemplateDir(src, dest, options) {
|
|
32
|
+
fse.ensureDirSync(dest);
|
|
33
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const srcPath = path.join(src, entry.name);
|
|
36
|
+
let destName = replaceInContent(entry.name, options);
|
|
37
|
+
if (destName.endsWith(".template")) {
|
|
38
|
+
destName = destName.slice(0, -9);
|
|
39
|
+
}
|
|
40
|
+
const destPath = path.join(dest, destName);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
copyTemplateDir(srcPath, destPath, options);
|
|
43
|
+
} else {
|
|
44
|
+
const content = fs.readFileSync(srcPath, "utf-8");
|
|
45
|
+
const processedContent = replaceInContent(content, options);
|
|
46
|
+
fs.writeFileSync(destPath, processedContent, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function main() {
|
|
51
|
+
program.name("@nubase/create").description("Create a new Nubase application").argument("[project-name]", "Name of the project").option("--db-name <name>", "Database name").option("--db-user <user>", "Database user").option("--db-password <password>", "Database password").option("--dev-port <port>", "Development database port", "5434").option("--test-port <port>", "Test database port", "5435").option("--backend-port <port>", "Backend server port", "3001").option("--frontend-port <port>", "Frontend dev server port", "3002").option("--skip-install", "Skip npm install").parse();
|
|
52
|
+
const args = program.args;
|
|
53
|
+
const opts = program.opts();
|
|
54
|
+
console.log(chalk.bold.cyan("\n Welcome to Nubase!\n"));
|
|
55
|
+
let projectName = args[0];
|
|
56
|
+
if (!projectName) {
|
|
57
|
+
const response = await prompts({
|
|
58
|
+
type: "text",
|
|
59
|
+
name: "projectName",
|
|
60
|
+
message: "What is your project name?",
|
|
61
|
+
initial: "my-app",
|
|
62
|
+
validate: (value) => /^[a-z0-9-]+$/.test(value) ? true : "Project name must be lowercase with hyphens only"
|
|
63
|
+
});
|
|
64
|
+
if (!response.projectName) {
|
|
65
|
+
console.log(chalk.red("\nProject name is required."));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
projectName = response.projectName;
|
|
69
|
+
}
|
|
70
|
+
const dbName = opts.dbName || toKebabCase(projectName).replace(/-/g, "_");
|
|
71
|
+
const dbUser = opts.dbUser || dbName;
|
|
72
|
+
const dbPassword = opts.dbPassword || dbName;
|
|
73
|
+
const options = {
|
|
74
|
+
name: projectName,
|
|
75
|
+
dbName,
|
|
76
|
+
dbUser,
|
|
77
|
+
dbPassword,
|
|
78
|
+
devPort: Number.parseInt(opts.devPort, 10),
|
|
79
|
+
testPort: Number.parseInt(opts.testPort, 10),
|
|
80
|
+
backendPort: Number.parseInt(opts.backendPort, 10),
|
|
81
|
+
frontendPort: Number.parseInt(opts.frontendPort, 10)
|
|
82
|
+
};
|
|
83
|
+
const targetDir = path.join(process.cwd(), projectName);
|
|
84
|
+
if (fs.existsSync(targetDir)) {
|
|
85
|
+
const response = await prompts({
|
|
86
|
+
type: "confirm",
|
|
87
|
+
name: "overwrite",
|
|
88
|
+
message: `Directory ${projectName} already exists. Overwrite?`,
|
|
89
|
+
initial: false
|
|
90
|
+
});
|
|
91
|
+
if (!response.overwrite) {
|
|
92
|
+
console.log(chalk.yellow("\nOperation cancelled."));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
fse.removeSync(targetDir);
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk.blue(`
|
|
98
|
+
Creating project in ${chalk.bold(targetDir)}...
|
|
99
|
+
`));
|
|
100
|
+
const templates = ["root", "schema", "backend", "frontend"];
|
|
101
|
+
for (const template of templates) {
|
|
102
|
+
const templatePath = path.join(TEMPLATE_DIR, template);
|
|
103
|
+
if (!fs.existsSync(templatePath)) {
|
|
104
|
+
console.log(chalk.yellow(`Template ${template} not found, skipping...`));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (template === "root") {
|
|
108
|
+
copyTemplateDir(templatePath, targetDir, options);
|
|
109
|
+
} else {
|
|
110
|
+
const destPath = path.join(
|
|
111
|
+
targetDir,
|
|
112
|
+
`${toKebabCase(projectName)}-${template}`
|
|
113
|
+
);
|
|
114
|
+
copyTemplateDir(templatePath, destPath, options);
|
|
115
|
+
}
|
|
116
|
+
console.log(chalk.green(` \u2713 Created ${template}`));
|
|
117
|
+
}
|
|
118
|
+
if (!opts.skipInstall) {
|
|
119
|
+
console.log(chalk.blue("\nInstalling dependencies...\n"));
|
|
120
|
+
try {
|
|
121
|
+
execSync("npm install", {
|
|
122
|
+
cwd: targetDir,
|
|
123
|
+
stdio: "inherit"
|
|
124
|
+
});
|
|
125
|
+
console.log(chalk.green("\n \u2713 Dependencies installed"));
|
|
126
|
+
} catch {
|
|
127
|
+
console.log(
|
|
128
|
+
chalk.yellow("\n \u26A0 Failed to install dependencies. Run npm install manually.")
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log(chalk.bold.green("\n Success! Your Nubase project is ready.\n"));
|
|
133
|
+
console.log(chalk.bold(" Next steps:\n"));
|
|
134
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
135
|
+
console.log(chalk.cyan(" npm run db:up # Start PostgreSQL"));
|
|
136
|
+
console.log(chalk.cyan(" npm run db:seed # Seed the database"));
|
|
137
|
+
console.log(chalk.cyan(" npm run dev # Start development\n"));
|
|
138
|
+
console.log(chalk.dim(" Database connection:"));
|
|
139
|
+
console.log(chalk.dim(` Host: localhost:${options.devPort}`));
|
|
140
|
+
console.log(chalk.dim(` Database: ${options.dbName}`));
|
|
141
|
+
console.log(chalk.dim(` User: ${options.dbUser}`));
|
|
142
|
+
console.log(chalk.dim(` Password: ${options.dbPassword}
|
|
143
|
+
`));
|
|
144
|
+
}
|
|
145
|
+
main().catch((error) => {
|
|
146
|
+
console.error(chalk.red("Error:"), error.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nubase/create",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Create a new Nubase application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nubase-create": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
15
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"lint": "biome lint .",
|
|
18
|
+
"lint:fix": "biome lint --write .",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"fs-extra": "^11.2.0",
|
|
25
|
+
"prompts": "^2.4.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/fs-extra": "^11.0.4",
|
|
29
|
+
"@types/node": "^22.10.2",
|
|
30
|
+
"@types/prompts": "^2.4.9",
|
|
31
|
+
"tsup": "^8.3.5",
|
|
32
|
+
"typescript": "^5.7.2"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"nubase",
|
|
39
|
+
"create",
|
|
40
|
+
"scaffold",
|
|
41
|
+
"fullstack",
|
|
42
|
+
"react",
|
|
43
|
+
"typescript"
|
|
44
|
+
],
|
|
45
|
+
"author": "",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/nubase/nubase.git",
|
|
53
|
+
"directory": "packages/create"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
DATABASE_URL="postgres://__DB_NAME___app:__DB_PASSWORD__@localhost:__TEST_PORT__/__DB_NAME__"
|
|
2
|
+
DATABASE_URL_ADMIN="postgres://__DB_USER__:__DB_PASSWORD__@localhost:__TEST_PORT__/__DB_NAME__"
|
|
3
|
+
DEBUG_AUTH_TOKEN=test-secret-123
|
|
4
|
+
JWT_SECRET=your-jwt-secret-change-in-production
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- __PROJECT_NAME_PASCAL__ Database Schema
|
|
2
|
+
-- This file is the source of truth for database structure
|
|
3
|
+
|
|
4
|
+
-- Create app user with restricted permissions (for RLS)
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
|
|
8
|
+
CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
|
|
9
|
+
END IF;
|
|
10
|
+
END
|
|
11
|
+
$$;
|
|
12
|
+
|
|
13
|
+
-- Enable RLS on the database
|
|
14
|
+
ALTER DATABASE __DB_NAME__ SET row_security = on;
|
|
15
|
+
|
|
16
|
+
-- ============================================================
|
|
17
|
+
-- WORKSPACES TABLE
|
|
18
|
+
-- ============================================================
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
20
|
+
id SERIAL PRIMARY KEY,
|
|
21
|
+
slug VARCHAR(100) UNIQUE NOT NULL,
|
|
22
|
+
name VARCHAR(255) NOT NULL,
|
|
23
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
24
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Grant permissions to app user
|
|
28
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
|
|
29
|
+
GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
|
|
30
|
+
|
|
31
|
+
-- ============================================================
|
|
32
|
+
-- USERS TABLE
|
|
33
|
+
-- ============================================================
|
|
34
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
35
|
+
id SERIAL PRIMARY KEY,
|
|
36
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
37
|
+
username VARCHAR(100) UNIQUE NOT NULL,
|
|
38
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
39
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
40
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Grant permissions to app user
|
|
44
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
|
|
45
|
+
GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
|
|
46
|
+
|
|
47
|
+
-- ============================================================
|
|
48
|
+
-- USER_WORKSPACES TABLE (Association with RLS)
|
|
49
|
+
-- ============================================================
|
|
50
|
+
CREATE TABLE IF NOT EXISTS user_workspaces (
|
|
51
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
52
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
53
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
54
|
+
PRIMARY KEY (user_id, workspace_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Enable RLS on user_workspaces
|
|
58
|
+
ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
|
|
60
|
+
-- Policy: Users can only see their own workspace associations
|
|
61
|
+
CREATE POLICY user_workspaces_isolation ON user_workspaces
|
|
62
|
+
USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
|
|
63
|
+
|
|
64
|
+
-- Grant permissions to app user
|
|
65
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
|
|
66
|
+
|
|
67
|
+
-- ============================================================
|
|
68
|
+
-- TICKETS TABLE (with RLS)
|
|
69
|
+
-- ============================================================
|
|
70
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
71
|
+
id SERIAL PRIMARY KEY,
|
|
72
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
73
|
+
title VARCHAR(255) NOT NULL,
|
|
74
|
+
description TEXT,
|
|
75
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
76
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Enable RLS on tickets
|
|
80
|
+
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
|
|
81
|
+
|
|
82
|
+
-- Policy: Users can only see tickets in their current workspace
|
|
83
|
+
CREATE POLICY tickets_workspace_isolation ON tickets
|
|
84
|
+
USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
|
|
85
|
+
|
|
86
|
+
-- Grant permissions to app user
|
|
87
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
|
|
88
|
+
GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
|
|
89
|
+
|
|
90
|
+
-- ============================================================
|
|
91
|
+
-- DEFAULT DATA
|
|
92
|
+
-- ============================================================
|
|
93
|
+
-- Create default workspace
|
|
94
|
+
INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
|
|
95
|
+
ON CONFLICT (slug) DO NOTHING;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgresql:
|
|
3
|
+
image: postgres:17.5
|
|
4
|
+
container_name: __PROJECT_NAME__-db-dev
|
|
5
|
+
ports:
|
|
6
|
+
- "__DEV_PORT__:5432"
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_DB: __DB_NAME__
|
|
9
|
+
POSTGRES_USER: __DB_USER__
|
|
10
|
+
POSTGRES_PASSWORD: __DB_PASSWORD__
|
|
11
|
+
volumes:
|
|
12
|
+
- postgresql-data:/var/lib/postgresql/data
|
|
13
|
+
- ./postgresql-init:/docker-entrypoint-initdb.d
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD-SHELL", "pg_isready -U __DB_USER__ -d __DB_NAME__"]
|
|
16
|
+
interval: 5s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 5
|
|
19
|
+
|
|
20
|
+
volumes:
|
|
21
|
+
postgresql-data:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- __PROJECT_NAME_PASCAL__ Database Schema
|
|
2
|
+
-- This file is the source of truth for database structure
|
|
3
|
+
|
|
4
|
+
-- Create app user with restricted permissions (for RLS)
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
|
|
8
|
+
CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
|
|
9
|
+
END IF;
|
|
10
|
+
END
|
|
11
|
+
$$;
|
|
12
|
+
|
|
13
|
+
-- Enable RLS on the database
|
|
14
|
+
ALTER DATABASE __DB_NAME__ SET row_security = on;
|
|
15
|
+
|
|
16
|
+
-- ============================================================
|
|
17
|
+
-- WORKSPACES TABLE
|
|
18
|
+
-- ============================================================
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
20
|
+
id SERIAL PRIMARY KEY,
|
|
21
|
+
slug VARCHAR(100) UNIQUE NOT NULL,
|
|
22
|
+
name VARCHAR(255) NOT NULL,
|
|
23
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
24
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Grant permissions to app user
|
|
28
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
|
|
29
|
+
GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
|
|
30
|
+
|
|
31
|
+
-- ============================================================
|
|
32
|
+
-- USERS TABLE
|
|
33
|
+
-- ============================================================
|
|
34
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
35
|
+
id SERIAL PRIMARY KEY,
|
|
36
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
37
|
+
username VARCHAR(100) UNIQUE NOT NULL,
|
|
38
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
39
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
40
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Grant permissions to app user
|
|
44
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
|
|
45
|
+
GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
|
|
46
|
+
|
|
47
|
+
-- ============================================================
|
|
48
|
+
-- USER_WORKSPACES TABLE (Association with RLS)
|
|
49
|
+
-- ============================================================
|
|
50
|
+
CREATE TABLE IF NOT EXISTS user_workspaces (
|
|
51
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
52
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
53
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
54
|
+
PRIMARY KEY (user_id, workspace_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Enable RLS on user_workspaces
|
|
58
|
+
ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
|
|
60
|
+
-- Policy: Users can only see their own workspace associations
|
|
61
|
+
CREATE POLICY user_workspaces_isolation ON user_workspaces
|
|
62
|
+
USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
|
|
63
|
+
|
|
64
|
+
-- Grant permissions to app user
|
|
65
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
|
|
66
|
+
|
|
67
|
+
-- ============================================================
|
|
68
|
+
-- TICKETS TABLE (with RLS)
|
|
69
|
+
-- ============================================================
|
|
70
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
71
|
+
id SERIAL PRIMARY KEY,
|
|
72
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
73
|
+
title VARCHAR(255) NOT NULL,
|
|
74
|
+
description TEXT,
|
|
75
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
76
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Enable RLS on tickets
|
|
80
|
+
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
|
|
81
|
+
|
|
82
|
+
-- Policy: Users can only see tickets in their current workspace
|
|
83
|
+
CREATE POLICY tickets_workspace_isolation ON tickets
|
|
84
|
+
USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
|
|
85
|
+
|
|
86
|
+
-- Grant permissions to app user
|
|
87
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
|
|
88
|
+
GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
|
|
89
|
+
|
|
90
|
+
-- ============================================================
|
|
91
|
+
-- DEFAULT DATA
|
|
92
|
+
-- ============================================================
|
|
93
|
+
-- Create default workspace
|
|
94
|
+
INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
|
|
95
|
+
ON CONFLICT (slug) DO NOTHING;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgresql:
|
|
3
|
+
image: postgres:17.5
|
|
4
|
+
container_name: __PROJECT_NAME__-db-test
|
|
5
|
+
ports:
|
|
6
|
+
- "__TEST_PORT__:5432"
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_DB: __DB_NAME__
|
|
9
|
+
POSTGRES_USER: __DB_USER__
|
|
10
|
+
POSTGRES_PASSWORD: __DB_PASSWORD__
|
|
11
|
+
volumes:
|
|
12
|
+
- postgresql-data:/var/lib/postgresql/data
|
|
13
|
+
- ./postgresql-init:/docker-entrypoint-initdb.d
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD-SHELL", "pg_isready -U __DB_USER__ -d __DB_NAME__"]
|
|
16
|
+
interval: 5s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 5
|
|
19
|
+
|
|
20
|
+
volumes:
|
|
21
|
+
postgresql-data:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- __PROJECT_NAME_PASCAL__ Database Schema
|
|
2
|
+
-- This file is the source of truth for database structure
|
|
3
|
+
|
|
4
|
+
-- Create app user with restricted permissions (for RLS)
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
|
|
8
|
+
CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
|
|
9
|
+
END IF;
|
|
10
|
+
END
|
|
11
|
+
$$;
|
|
12
|
+
|
|
13
|
+
-- Enable RLS on the database
|
|
14
|
+
ALTER DATABASE __DB_NAME__ SET row_security = on;
|
|
15
|
+
|
|
16
|
+
-- ============================================================
|
|
17
|
+
-- WORKSPACES TABLE
|
|
18
|
+
-- ============================================================
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
20
|
+
id SERIAL PRIMARY KEY,
|
|
21
|
+
slug VARCHAR(100) UNIQUE NOT NULL,
|
|
22
|
+
name VARCHAR(255) NOT NULL,
|
|
23
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
24
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Grant permissions to app user
|
|
28
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
|
|
29
|
+
GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
|
|
30
|
+
|
|
31
|
+
-- ============================================================
|
|
32
|
+
-- USERS TABLE
|
|
33
|
+
-- ============================================================
|
|
34
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
35
|
+
id SERIAL PRIMARY KEY,
|
|
36
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
37
|
+
username VARCHAR(100) UNIQUE NOT NULL,
|
|
38
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
39
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
40
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Grant permissions to app user
|
|
44
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
|
|
45
|
+
GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
|
|
46
|
+
|
|
47
|
+
-- ============================================================
|
|
48
|
+
-- USER_WORKSPACES TABLE (Association with RLS)
|
|
49
|
+
-- ============================================================
|
|
50
|
+
CREATE TABLE IF NOT EXISTS user_workspaces (
|
|
51
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
52
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
53
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
54
|
+
PRIMARY KEY (user_id, workspace_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Enable RLS on user_workspaces
|
|
58
|
+
ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
|
|
60
|
+
-- Policy: Users can only see their own workspace associations
|
|
61
|
+
CREATE POLICY user_workspaces_isolation ON user_workspaces
|
|
62
|
+
USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
|
|
63
|
+
|
|
64
|
+
-- Grant permissions to app user
|
|
65
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
|
|
66
|
+
|
|
67
|
+
-- ============================================================
|
|
68
|
+
-- TICKETS TABLE (with RLS)
|
|
69
|
+
-- ============================================================
|
|
70
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
71
|
+
id SERIAL PRIMARY KEY,
|
|
72
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
73
|
+
title VARCHAR(255) NOT NULL,
|
|
74
|
+
description TEXT,
|
|
75
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
76
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Enable RLS on tickets
|
|
80
|
+
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
|
|
81
|
+
|
|
82
|
+
-- Policy: Users can only see tickets in their current workspace
|
|
83
|
+
CREATE POLICY tickets_workspace_isolation ON tickets
|
|
84
|
+
USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
|
|
85
|
+
|
|
86
|
+
-- Grant permissions to app user
|
|
87
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
|
|
88
|
+
GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
|
|
89
|
+
|
|
90
|
+
-- ============================================================
|
|
91
|
+
-- DEFAULT DATA
|
|
92
|
+
-- ============================================================
|
|
93
|
+
-- Create default workspace
|
|
94
|
+
INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
|
|
95
|
+
ON CONFLICT (slug) DO NOTHING;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "NODE_ENV=development tsx watch src/index.ts",
|
|
7
|
+
"dev:test": "PORT=__BACKEND_PORT__ DB_PORT=__TEST_PORT__ NODE_ENV=test tsx watch src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"db:dev:up": "docker compose -f docker/dev/docker-compose.yml up -d",
|
|
11
|
+
"db:dev:down": "docker compose -f docker/dev/docker-compose.yml down",
|
|
12
|
+
"db:dev:kill": "docker compose -f docker/dev/docker-compose.yml down -v",
|
|
13
|
+
"db:dev:seed": "NODE_ENV=development tsx src/db/seed.ts",
|
|
14
|
+
"db:test:up": "docker compose -f docker/test/docker-compose.yml up -d",
|
|
15
|
+
"db:test:down": "docker compose -f docker/test/docker-compose.yml down",
|
|
16
|
+
"db:test:kill": "docker compose -f docker/test/docker-compose.yml down -v",
|
|
17
|
+
"db:seed": "tsx src/db/seed.ts",
|
|
18
|
+
"db:schema-sync": "cp db/schema.sql docker/dev/postgresql-init/dump.sql && cp db/schema.sql docker/test/postgresql-init/dump.sql",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"lint:fix": "biome check . --write --unsafe",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@hono/node-server": "^1.13.7",
|
|
27
|
+
"@nubase/backend": "*",
|
|
28
|
+
"bcrypt": "^5.1.1",
|
|
29
|
+
"dotenv": "^16.4.7",
|
|
30
|
+
"drizzle-orm": "^0.38.3",
|
|
31
|
+
"hono": "^4.6.14",
|
|
32
|
+
"jsonwebtoken": "^9.0.2",
|
|
33
|
+
"pg": "^8.13.1",
|
|
34
|
+
"__PROJECT_NAME__-schema": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@faker-js/faker": "^9.3.0",
|
|
38
|
+
"@types/bcrypt": "^5.0.2",
|
|
39
|
+
"@types/jsonwebtoken": "^9.0.7",
|
|
40
|
+
"@types/node": "^22.10.2",
|
|
41
|
+
"@types/pg": "^8.11.10",
|
|
42
|
+
"drizzle-kit": "^0.30.1",
|
|
43
|
+
"tsx": "^4.19.2",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"vitest": "^2.1.8"
|
|
46
|
+
}
|
|
47
|
+
}
|