init repo

This commit is contained in:
b1ek 2024-11-10 01:01:54 +10:00
commit f8f94217ee
Signed by: blek
GPG Key ID: A622C22C9BC616B2
44 changed files with 3208 additions and 0 deletions

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
APP_DEBUG=true
DB_PASS=db
DB_NAME=db
DB_USER=db
DB_HOST=db

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/docker-compose.yml
/.env
node_modules
dist

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# digital.solutions.test
to start up
```
cp docker-compose.yml.dev docker-compose.yml
docker-compose up -d
```

18
back/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to docker",
"restart": true,
"localRoot": ".",
"remoteRoot": "/app",
"address": "localhost",
"port": 9229
}
]
}

29
back/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "back",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"license": "GPL-3.0-only",
"private": true,
"devDependencies": {
"@types/express": "^5.0.0",
"@types/express-session": "^1.18.0",
"@types/node": "^22.9.0",
"esbuild": "^0.24.0"
},
"scripts": {
"build": "node scripts/build.js",
"start": "yarn build && node dist/index.js",
"typeorm": "yarn build && typeorm -d dist/typeorm/data-source.repo.js",
"docker": "docker-compose exec back yarn"
},
"dependencies": {
"connect-typeorm": "^2.0.0",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-session": "^1.18.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.20"
}
}

13
back/scripts/build.js Normal file
View File

@ -0,0 +1,13 @@
import esbuild from 'esbuild';
import fsp from 'node:fs/promises';
fsp.rm('dist', { recursive: true, force: true });
fsp.mkdir('dist', { recursive: true });
const src = await fsp.readdir('src', { recursive: true });
const filtered = src.filter(x => x.endsWith('.ts')).map(x => 'src/' + x);
await esbuild.build({
entryPoints: filtered,
outdir: 'dist',
tsconfig: 'tsconfig.json'
});

8
back/src/env.ts Normal file
View File

@ -0,0 +1,8 @@
import * as dotenv from 'dotenv';
dotenv.config({
path: [
'/.env.global',
'/app/.env'
]
})

36
back/src/index.ts Normal file
View File

@ -0,0 +1,36 @@
import express from 'express';
import session from 'express-session';
import 'reflect-metadata';
import routes from './routes.js';
import dataSourceRepo from './typeorm/data-source.repo.js';
import './env.js';
import { TypeORMError } from 'typeorm';
import { TypeormStore } from 'connect-typeorm';
const listen_host = process.env.LISTEN_HOST ?? '0.0.0.0';
const listen_port = parseInt(process.env.LISTEN_PORT ?? '80');
await dataSourceRepo.initialize();
await dataSourceRepo.runMigrations();
const app = express();
app.use(express.json());
app.use(session({
resave: false,
saveUninitialized: false,
store: new TypeormStore({
cleanupLimit: 2,
limitSubquery: false,
ttl: 3600
}),
secret: 'keyboard cat'
}));
app.use('/api', routes());
app.listen(listen_port, listen_host, () => {
console.log(`Listening on ${listen_host}:${listen_port}`)
});

12
back/src/routes.ts Normal file
View File

@ -0,0 +1,12 @@
import { Router } from 'express';
import data from './routes/data.js';
import selection from './routes/selection.js';
export default function(): Router {
const app = Router();
app.use('/data', data());
app.use('/selection', selection())
return app;
}

49
back/src/routes/data.ts Normal file
View File

@ -0,0 +1,49 @@
import { IRouter, Router } from 'express';
import dataSourceRepo from '../typeorm/data-source.repo.js';
import { DataRow } from '../typeorm/entity/DataRow.entity.js';
export default function(): IRouter {
const app = Router();
app.get('/list', async (req, res) => {
const [page, pageSz, search] = [
parseInt(req.query.page as string | undefined),
parseInt((req.query.page_sz ?? '20').toString()),
req.query.search as string | undefined
];
if (pageSz <= 0) {
res.status(400);
res.send('invalid page_sz');
return;
}
const query = dataSourceRepo.getRepository(DataRow).createQueryBuilder('data');
if (!Number.isNaN(page)) {
query.take(pageSz);
query.skip(page * pageSz);
}
if (search !== undefined) {
query.where('data like :search', { search });
}
const data = await query.getMany();
res.send(data);
})
app.put('/', async (req, res) => {
const data = req.body.data;
if (!data) {
res.send(400);
return;
}
const row = new DataRow();
row.data = data;
await dataSourceRepo.getRepository(DataRow).insert(row);
res.status(200);
res.send('');
return;
})
return app;
}

View File

@ -0,0 +1,6 @@
import { IRouter, Router } from "express";
export default function(): IRouter {
const app = Router();
return app;
}

View File

@ -0,0 +1,15 @@
import { DataSource } from "typeorm";
import '../env.js';
export default new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
entities: [ import.meta.dirname + "/entity/*.js" ],
migrations: [ import.meta.dirname + "/migration/*.js" ],
logging: true,
synchronize: true
});

View File

@ -0,0 +1,11 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import 'reflect-metadata';
@Entity()
export class DataRow {
@PrimaryGeneratedColumn()
id: number
@Column('varchar')
data: string
}

View File

@ -0,0 +1,17 @@
import { Column, DeleteDateColumn, Entity, Index, PrimaryColumn } from "typeorm";
@Entity()
export class Session {
@Index()
@Column('bigint')
public expiredAt = Date.now();
@PrimaryColumn('varchar', { length: 255 })
public id = '';
@Column('text')
public json = '';
@DeleteDateColumn()
public destroyedAt?: Date;
}

View File

@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
export class Datarow1731074164993 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
queryRunner.createTable(
new Table({
name: 'DataRow',
columns: [
{
name: 'id',
type: 'bigint',
isGenerated: true,
isPrimary: true,
isNullable: false,
},
{
name: 'data',
type: 'varchar',
isNullable: false
}
]
}),
true
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
queryRunner.dropTable('DataRow', true)
}
}

View File

@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
export class Session1731074164994 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
queryRunner.createTable(
new Table({
name: 'Session',
columns: [
{
name: 'expiredAt',
type: 'bigint',
isGenerated: true,
isPrimary: true,
isNullable: false,
},
{
name: 'id',
type: 'varchar',
length: '255',
isNullable: false,
isUnique: true
},
{
name: 'json',
type: 'varchar',
isNullable: false
},
{
name: 'destroyedAt',
type: 'date',
isNullable: true
}
]
}),
true
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
queryRunner.dropTable('Session', true)
}
}

5
back/start.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
yarn
yarn build
node dist/index.js --inspect=0.0.0.0:9229

10
back/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"lib": ["es5", "es6", "dom"],
"target": "es2017",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}

1368
back/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

6
config/caddy/Caddyfile Normal file
View File

@ -0,0 +1,6 @@
:80 {
route /api/* {
reverse_proxy http://back
}
reverse_proxy http://front
}

57
docker-compose.yml.dev Normal file
View File

@ -0,0 +1,57 @@
services:
back:
image: node:22-alpine3.20
# restart: always
ports:
- 9229:9229
networks:
internal:
aliases:
- back
entrypoint: '/app/start.sh'
working_dir: '/app'
volumes:
- './back:/app'
- 'back-node-modules:/app/node_modules'
- './.env:/.env.global:ro'
front:
build:
context: front
dockerfile: Dockerfile.dev
networks:
internal:
aliases:
- front
volumes:
- './front:/app'
- 'front-node-modules:/app/node_modules'
db:
image: postgres:17-alpine
volumes:
- 'db-data:/var/lib/postgresql'
environment:
POSTGRES_PASSWORD: '${DB_PASS}'
POSTGRES_USER: '${DB_USER}'
POSTGRES_DB: '${DB_NAME}'
ports:
- 5432:5432
networks:
internal:
aliases:
- '${DB_HOST}'
server:
image: caddy:2.8.4-alpine
ports:
- 80:80
volumes:
- './config/caddy:/etc/caddy:ro'
- './volatile/caddy/log:/var/log/caddy'
networks:
internal:
networks:
internal:
volumes:
'back-node-modules':
'front-node-modules':
'db-data':

3
front/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
Dockerfile
Dockerfile.dev
.dockerignore

24
front/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
front/Dockerfile.dev Normal file
View File

@ -0,0 +1,8 @@
FROM node:22-alpine3.20
WORKDIR /app
COPY . .
RUN yarn
CMD [ "yarn", "dev", "--host=front", "--port=80" ]

13
front/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Preact + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

22
front/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "front",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"ky": "^1.7.2",
"preact": "^10.24.3"
},
"devDependencies": {
"@preact/preset-vite": "^2.9.1",
"sass": "^1.80.6",
"sass-embedded": "^1.80.6",
"typescript": "~5.6.2",
"vite": "^5.4.10"
}
}

27
front/src/api/DataRow.ts Normal file
View File

@ -0,0 +1,27 @@
import ky from "ky";
export type DataRowSearchOptions = {
page?: number,
page_sz?: number,
search?: string
}
export class DataRow {
constructor(
public id: number,
public data: string
) { }
static reviveJSON(data: { id: number, data: string }): DataRow {
return new DataRow(data.id, data.data);
}
static async find(options?: DataRowSearchOptions): Promise<DataRow[]> {
const req = await ky('/api/data/list', {
searchParams: options
});
const raw = await req.json<any[]>();
return raw.map(this.reviveJSON);
}
}

10
front/src/app.tsx Normal file
View File

@ -0,0 +1,10 @@
import { Index } from "./pages";
export function App() {
return (
<>
<h1>4chan ripoff</h1>
<Index />
</>
)
}

View File

View File

@ -0,0 +1,3 @@
.posts {
display: block;
}

View File

@ -0,0 +1,13 @@
import { DataRow } from "../api/DataRow";
import style from './Posts.module.scss';
import { Post } from "./display/Post";
export type PostsProps = { rows: DataRow[] };
export function Posts({ rows }: PostsProps) {
return (
<div className={style.posts}>
{ rows.map(row => <Post row={row} />) }
</div>
);
}

View File

@ -0,0 +1,19 @@
import { TargetedEvent } from 'preact/compat';
export type PaginatorProps = {
page: number,
onChange: (page: number) => void,
className?: string
};
export function Paginator({page, onChange, className}: PaginatorProps) {
function middleware(e: TargetedEvent<HTMLInputElement, Event>) {
onChange(parseInt(e.currentTarget.value));
}
return (
<div className={className}>
<label for='page'>Page: </label>
<input type='number' id='page' value={page} onChange={middleware}/>
</div>
)
}

View File

@ -0,0 +1,8 @@
.post {
border: 1px solid gray;
padding: 0.5rem;
margin: 0.5rem 0;
pre {
padding: 0; margin: 0;
}
}

View File

@ -0,0 +1,11 @@
import { DataRow } from "../../api/DataRow";
import style from './Post.module.scss';
export type PostProps = { row: DataRow };
export function Post({ row }: PostProps) {
return (
<div id={`post-${row.id}`} className={style.post}>
<pre>{row.data}</pre>
</div>
)
}

4
front/src/main.tsx Normal file
View File

@ -0,0 +1,4 @@
import { render } from 'preact'
import { App } from './app.tsx'
render(<App />, document.getElementById('app')!)

View File

@ -0,0 +1,6 @@
.index {
.paginator {
border-bottom: 1px solid black;
padding-bottom: 1rem;
}
}

63
front/src/pages/index.tsx Normal file
View File

@ -0,0 +1,63 @@
import { useEffect, useState } from "preact/hooks";
import { DataRow } from "../api/DataRow";
import style from './index.module.scss';
import { Posts } from "../components/Posts";
import { Paginator } from "../components/display/Paginator";
const cachedPages: {
[ key: number ]: DataRow[]
} = {};
let firstRender = true;
export function Index() {
const [ data, setData ] = useState(null as null | DataRow[]);
const [ page, setPage ] = useState(NaN);
const [ pageBlocked, setPageBlocked ] = useState(false);
async function goToPage(newPage: number) {
if (newPage < 0) newPage = 0;
if (pageBlocked) return;
let newData = null as DataRow[] | null;
if (cachedPages[newPage]) {
newData = cachedPages[newPage];
} else {
setData(null);
}
setPage(newPage);
setPageBlocked(true);
try {
newData = await DataRow.find({
page: newPage
});
} catch (_) {
return setPageBlocked(false)
}
if (newData === undefined) {
newData = [] as DataRow[];
}
cachedPages[newPage] = newData;
setPageBlocked(false);
setData(newData);
}
if (firstRender) {
goToPage(0);
firstRender = false;
}
return (
<div className={style.index}>
<Paginator page={page} onChange={goToPage} className={style.paginator} />
{
data
? <Posts rows={data} />
: <pre>loading . . .</pre>
}
</div>
)
}

1
front/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
front/tsconfig.app.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
},
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
front/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
front/tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
front/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [preact()],
})

1148
front/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

2
volatile/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore