Merge pull request #69 from parcelvoy/feat/local-storage

Local Storage
This commit is contained in:
Chris Hills 2023-03-08 20:11:40 -06:00 committed by GitHub
commit 38218c5f70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 24825 additions and 66286 deletions

View file

@ -2,6 +2,8 @@ name: Create & Publish Docker Image
on:
push:
branches:
- "main"
tags:
- "v*.*.*"

View file

@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- uses: actions/checkout@v1
@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
services:
mysql:
@ -48,10 +48,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Node 16.x
- name: Node 18.x
uses: actions/setup-node@v2
with:
node-version: 16.x
node-version: 18.x
- name: Cache NPM
uses: actions/cache@v2

View file

@ -107,3 +107,7 @@ build
# VS Code
.DS_Store
# Uploads
/public/uploads/*
!/public/uploads/.gitkeep

View file

@ -13,6 +13,7 @@ COPY --from=compile /usr/src/app/apps/platform/package*.json ./
COPY --from=compile /usr/src/app/apps/platform/build ./
COPY --from=compile /usr/src/app/apps/platform/db ./db
COPY --from=compile /usr/src/app/apps/platform/scripts ./scripts
COPY --from=compile /usr/src/app/apps/platform/public ./public
RUN npm ci --only=production
# --------------> The production image

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@
"koa": "^2.13.4",
"koa-body": "5.0.0",
"koa-jwt": "^4.0.3",
"koa-static": "^5.0.0",
"mysql2": "^2.3.3",
"node-pushnotifications": "^2.0.3",
"node-schedule": "^2.1.0",
@ -42,7 +43,7 @@
"start": "nodemon",
"build": "tsc --build",
"lint": "eslint --ext .ts --max-warnings 0 src/",
"test": "jest --forceExit --testTimeout 10000",
"test": "jest --forceExit --runInBand --testTimeout 10000",
"docker:build": "docker buildx build -f ./Dockerfile -t ghcr.io/parcelvoy/api:latest ../../",
"docker:build:push": "npm run docker:build -- --push",
"migration:create": "node ./scripts/create-migration.mjs"
@ -54,6 +55,7 @@
"@types/jsonwebtoken": "^8.5.9",
"@types/koa__cors": "^3.3.0",
"@types/koa__router": "^8.0.11",
"@types/koa-static": "^4.0.2",
"@types/node": "^18.7.18",
"@types/node-pushnotifications": "^1.0.4",
"@types/node-schedule": "^2.1.0",

View file

View file

@ -1,6 +1,7 @@
import Koa from 'koa'
import koaBody from 'koa-body'
import cors from '@koa/cors'
import serve from 'koa-static'
import controllers from './config/controllers'
import { RequestError } from './core/errors'
@ -18,6 +19,10 @@ export default class Api extends Koa {
ctx.state.app = this.app
return next()
})
.use(serve('./public', {
hidden: true,
defer: true,
}))
this.use(async (ctx, next) => {
try {

View file

@ -205,7 +205,7 @@ describe('CampaignService', () => {
send_at: new Date(),
}) as SentCampaign
const inclusiveIds = []
const inclusiveIds: number[] = []
for (let i = 0; i < 20; i++) {
const user = await createUser(params.project_id)
await addUserToList(user, list)

View file

@ -35,7 +35,8 @@ const connect = (config: DatabaseConfig, withDB = true) => {
})
}
const migrate = async (db: Database) => {
const migrate = async (db: Database, fresh = false) => {
if (fresh) await db.raw('CREATE DATABASE parcelvoy')
return db.migrate.latest({
directory: './db/migrations',
tableName: 'migrations',
@ -53,7 +54,7 @@ export default async (config: DatabaseConfig) => {
// On error, try to create the database and try again
const db = connect(config, false)
db.raw('CREATE DATABASE parcelvoy')
await migrate(db, true)
return connect(config)
}
}

View file

@ -53,19 +53,20 @@ export default (type?: EnvType): Env => {
},
}),
}),
storage: {
baseUrl: process.env.STORAGE_BASE_URL!,
...driver<Omit<StorageConfig, 'baseUrl'>>(process.env.STORAGE_DRIVER, {
s3: () => ({
bucket: process.env.AWS_S3_BUCKET!,
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
storage: driver<StorageConfig>(process.env.STORAGE_DRIVER ?? 'local', {
s3: () => ({
baseUrl: process.env.STORAGE_BASE_URL,
bucket: process.env.AWS_S3_BUCKET!,
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
},
local: () => ({
baseUrl: process.env.STORAGE_BASE_URL,
}),
}),
baseUrl: process.env.BASE_URL!,
port: parseInt(process.env.PORT!),
secret: process.env.APP_SECRET!,

View file

@ -218,13 +218,17 @@ export default class Model {
}
}
static async insert<T extends typeof Model>(this: T, data: Partial<InstanceType<T>>, db?: Database): Promise<number>
static async insert<T extends typeof Model>(this: T, data: Partial<InstanceType<T>>[], db?: Database): Promise<number[]>
static async insert<T extends typeof Model>(
this: T,
data: Partial<InstanceType<T>> | Partial<InstanceType<T>>[] = {},
db: Database = App.main.db,
): Promise<number> {
): Promise<number | number[]> {
const formattedData = this.formatJson(data)
return await this.table(db).insert(formattedData)
const value = await this.table(db).insert(formattedData)
if (Array.isArray(data)) return value
return value[0]
}
static async insertAndFetch<T extends typeof Model>(
@ -232,8 +236,7 @@ export default class Model {
data: Partial<InstanceType<T>> = {},
db: Database = App.main.db,
): Promise<InstanceType<T>> {
const formattedData = this.formatJson(data)
const id: number = await this.table(db).insert(formattedData)
const id = await this.insert(data, db)
return await this.find(id, b => b, db) as InstanceType<T>
}

View file

@ -82,7 +82,7 @@ export const openWrapHtml = (html: string, params: TrackedLinkParams) => {
const imageHtml = `<img border="0" width="1" height="1" alt="" src="${link}" />`
const hasBody = html.includes(bodyTag)
if (hasBody) {
html.replace(bodyTag, imageHtml + bodyTag)
html = html.replace(bodyTag, (imageHtml + bodyTag))
} else {
html += imageHtml
}

View file

@ -1,5 +1,5 @@
import Model from '../core/Model'
import { combineURLs } from '../utilities'
import Storage from './Storage'
export default class Image extends Model {
project_id!: number
@ -15,7 +15,7 @@ export default class Image extends Model {
}
get url(): string {
return combineURLs([process.env.STORAGE_BASE_URL!, this.filename])
return Storage.url(this.filename)
}
toJSON() {

View file

@ -0,0 +1,26 @@
import fs from 'fs/promises'
import path from 'path'
import { StorageTypeConfig } from './Storage'
import { ImageUploadTask, StorageProvider } from './StorageProvider'
export interface LocalConfig extends StorageTypeConfig {
driver: 'local'
}
export class LocalStorageProvider implements StorageProvider {
path(filename: string) {
return path.join(process.cwd(), 'public', 'uploads', filename)
}
async upload(task: ImageUploadTask) {
await fs.writeFile(
task.url,
task.stream,
)
}
async delete(filename: string): Promise<void> {
await fs.unlink(filename)
}
}

View file

@ -6,6 +6,7 @@ import { StorageTypeConfig } from './Storage'
import { ImageUploadTask, StorageProvider } from './StorageProvider'
export interface S3Config extends StorageTypeConfig, AWSConfig {
driver: 's3'
bucket: string
}
@ -17,6 +18,10 @@ export class S3StorageProvider implements StorageProvider {
this.config = config
}
path(filename: string) {
return filename
}
async upload(task: ImageUploadTask) {
const pass = new PassThrough()
const s3 = new S3(this.config)

View file

@ -8,10 +8,12 @@ import { combineURLs, uuid } from '../utilities'
import { InternalError } from '../core/errors'
import StorageError from './StorageError'
import App from '../app'
import { LocalConfig, LocalStorageProvider } from './LocalStorageProvider'
export type StorageConfig = S3Config & { baseUrl: string }
export type StorageConfig = S3Config | LocalConfig
export interface StorageTypeConfig extends DriverConfig {
driver: StorageProviderName
baseUrl?: string
}
export interface ImageUpload {
@ -24,6 +26,8 @@ export default class Storage {
constructor(config?: StorageConfig) {
if (config?.driver === 's3') {
this.provider = new S3StorageProvider(config)
} else if (config?.driver === 'local') {
this.provider = new LocalStorageProvider()
} else {
throw new InternalError(StorageError.UndefinedStorageMethod)
}
@ -34,7 +38,7 @@ export default class Storage {
const originalPath = path.parse(image.metadata.fileName)
const extension = originalPath.ext
const fileName = originalPath.name
const url = `${key}${extension}`
const url = this.provider.path(`${key}${extension}`)
await this.upload({
stream: image.file,
@ -53,7 +57,14 @@ export default class Storage {
await this.provider.upload(task)
}
url(path: string): string {
return combineURLs([App.main.env.storage.baseUrl, path])
static url(path: string): string {
// If an override is provide, utilize that
if (App.main.env.storage.baseUrl) {
return combineURLs([App.main.env.storage.baseUrl, path])
}
// Otherwise default back to local path
return `/uploads/${path}`
}
}

View file

@ -1,6 +1,6 @@
import { Stream } from 'stream'
export type StorageProviderName = 's3'
export type StorageProviderName = 's3' | 'local'
export interface ImageUploadTask {
stream: Stream
@ -8,6 +8,7 @@ export interface ImageUploadTask {
}
export interface StorageProvider {
path(filename: string): string
upload(task: ImageUploadTask): Promise<void>
delete(filename: string): Promise<void>
}

View file

@ -1,5 +1,5 @@
# --------------> The compiler image
FROM node:16 AS compile
FROM node:18 AS compile
WORKDIR /usr/src/app/apps/ui
COPY ./tsconfig.base.json /usr/src/app
COPY ./apps/ui ./

View file

@ -10,4 +10,10 @@ server {
location /api {
proxy_pass http://api:3001;
}
location /uploads {
proxy_pass http://api:3001;
}
client_max_body_size 64M;
}

13541
apps/ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
"version": "0.1.0",
"dependencies": {
"@fontsource/inter": "^4.5.14",
"@headlessui/react": "^1.7.3",
"@headlessui/react": "1.7.3",
"@heroicons/react": "^2.0.11",
"@monaco-editor/react": "^4.4.6",
"@popperjs/core": "^2.11.6",
@ -26,7 +26,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.33.0",
"react-hot-toast": "2.4.0",
"react-hot-toast": "^2.4.0",
"react-popper": "^2.3.0",
"react-router-dom": "^6.4.2",
"react-scripts": "5.0.1",

View file

@ -63,6 +63,7 @@
.modal.fullscreen .modal-content {
flex-grow: 1;
position: relative;
}
.modal-inner .modal-header h3 {

View file

@ -1,7 +1,6 @@
.email-editor {
height: 100vh;
background: var(--color-background);
position: fixed;
position: absolute;
display: flex;
flex-direction: column;
grid-template-rows: 60px auto auto;

View file

@ -168,20 +168,22 @@ export default function EmailEditor() {
open
onClose={() => navigate(`../campaigns/${campaign.id}/design`)}
>
<Tabs
selectedIndex={selectedIndex}
onChange={setSelectedIndex}
tabs={tabs}
append={
<Button
size="small"
variant="secondary"
onClick={() => setIsAddLocaleOpen(true)}
>
{'Add Locale'}
</Button>
}
/>
<section className="email-editor">
<Tabs
selectedIndex={selectedIndex}
onChange={setSelectedIndex}
tabs={tabs}
append={
<Button
size="small"
variant="secondary"
onClick={() => setIsAddLocaleOpen(true)}
>
{'Add Locale'}
</Button>
}
/>
</section>
</Modal>
<CreateLocaleModal
open={isAddLocaleOpen}

View file

@ -36,7 +36,7 @@
}
.image-gallery .image img {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
object-fit: cover;
}

View file

@ -28,7 +28,7 @@ services:
NODE_ENV: ${NODE_ENV}
BASE_URL: ${BASE_URL}
APP_SECRET: ${APP_SECRET}
PORT: 3000
PORT: 3001
DB_CLIENT: ${DB_CLIENT}
DB_HOST: mysql
DB_USERNAME: ${DB_USERNAME}
@ -41,7 +41,7 @@ services:
QUEUE_DRIVER: ${QUEUE_DRIVER}
AWS_SQS_QUEUE_URL: ${AWS_SQS_QUEUE_URL}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${S3_LOCAWS_SECRET_ACCESS_KEYAL_STACK}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_REGION: ${AWS_REGION}
AUTH_DRIVER: ${AUTH_DRIVER}
AUTH_SAML_CALLBACK_URL: ${AUTH_SAML_CALLBACK_URL}
@ -54,6 +54,8 @@ services:
AUTH_OPENID_CLIENT_SECRET: ${AUTH_OPENID_CLIENT_SECRET}
AUTH_OPENID_REDIRECT_URI: ${AUTH_OPENID_REDIRECT_URI}
AUTH_OPENID_DOMAIN_WHITELIST: ${AUTH_OPENID_DOMAIN_WHITELIST}
volumes:
- uploads:/usr/src/app/public/uploads
ui:
image: 'ghcr.io/parcelvoy/ui:latest'
depends_on:
@ -64,4 +66,6 @@ services:
- 80:3000
volumes:
mysql_data:
driver: local
uploads:
driver: local

65188
package-lock.json generated

File diff suppressed because it is too large Load diff