Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 New environment variables: DB path / Public path / Show OPEN API #307

Merged
merged 4 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-cats-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'manifest': patch
---

added dynamic path for db and public folder
3 changes: 2 additions & 1 deletion packages/core/manifest/src/config/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export default (): { database: SqliteConnectionOptions } => {
return {
database: {
type: 'sqlite',
database: `${process.cwd()}/manifest/backend.db`,
database:
process.env.DATABASE_PATH || `${process.cwd()}/manifest/backend.db`,
dropSchema: process.env.DB_DROP_SCHEMA === 'true' || false,
synchronize: true
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/manifest/src/config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path'
export default (): {
paths: {
adminPanelFolder: string
publicFolder: string
manifestFile: string
handlersFolder: string
}
Expand All @@ -13,6 +14,7 @@ export default (): {
process.env.NODE_ENV === 'contribution'
? path.join(process.cwd(), '..', 'admin', 'dist')
: `${process.cwd()}/node_modules/manifest/dist/admin`,
publicFolder: process.env.PUBLIC_FOLDER || `${process.cwd()}/public`,
manifestFile:
process.env.MANIFEST_FILE_PATH ||
`${process.cwd()}/manifest/backend.yml`,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/manifest/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@repo/types'

// Paths.
export const STORAGE_PATH: string = 'public/storage'
export const STORAGE_PATH: string = 'storage'
export const API_PATH: string = 'api'
export const COLLECTIONS_PATH: string = 'collections'
export const SINGLES_PATH: string = 'singles'
Expand Down
34 changes: 25 additions & 9 deletions packages/core/manifest/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import * as express from 'express'
import * as livereload from 'livereload'
import { join } from 'path'
import { AppModule } from './app.module'
import { API_PATH, DEFAULT_PORT, DEFAULT_TOKEN_SECRET_KEY } from './constants'
import {
API_PATH,
DEFAULT_PORT,
DEFAULT_TOKEN_SECRET_KEY,
STORAGE_PATH
} from './constants'
import { OpenApiService } from './open-api/services/open-api.service'

async function bootstrap() {
Expand Down Expand Up @@ -49,24 +54,34 @@ async function bootstrap() {
const adminPanelFolder: string = configService.get('paths').adminPanelFolder
app.use(express.static(adminPanelFolder))

app.use('/storage', express.static('public/storage'))
const publicFolder: string = configService.get('paths').publicFolder
const storagePath = join(publicFolder, STORAGE_PATH)

app.use(`/${STORAGE_PATH}`, express.static(storagePath))

// Redirect all requests to the client app index.
app.use((req, res, next) => {
if (req.url.startsWith(`/${API_PATH}`) || req.url.startsWith('/storage')) {
if (
req.url.startsWith(`/${API_PATH}`) ||
req.url.startsWith(`/${STORAGE_PATH}`)
) {
next()
} else {
res.sendFile(join(adminPanelFolder, 'index.html'))
}
})

const openApiService: OpenApiService = app.get(OpenApiService)
// Open API documentation.
const showOpenApi: boolean = process.env.OPEN_API_DOCS !== 'false'

if (showOpenApi) {
const openApiService: OpenApiService = app.get(OpenApiService)

SwaggerModule.setup(API_PATH, app, openApiService.generateOpenApiObject(), {
customfavIcon: 'assets/images/open-api/favicon.ico',
customSiteTitle: 'Manifest API Doc',
SwaggerModule.setup(API_PATH, app, openApiService.generateOpenApiObject(), {
customfavIcon: 'assets/images/open-api/favicon.ico',
customSiteTitle: 'Manifest API Doc',

customCss: `
customCss: `

.swagger-ui html {
box-sizing: border-box;
Expand Down Expand Up @@ -1782,7 +1797,8 @@ background: #ce107c;
fill: #535356;
}
`
})
})
}

await app.listen(configService.get('PORT') || DEFAULT_PORT)
}
Expand Down
22 changes: 15 additions & 7 deletions packages/core/manifest/src/storage/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export class StorageService {

const filePath: string = `${folder}/${uniqid()}-${slugify(file.originalname)}`

fs.writeFileSync(`${STORAGE_PATH}/${filePath}`, file.buffer)
fs.writeFileSync(
`${this.configService.get('paths').publicFolder}/${STORAGE_PATH}/${filePath}`,
file.buffer
)

return this.prependStorageUrl(filePath)
}
Expand Down Expand Up @@ -70,9 +73,12 @@ export class StorageService {
.resize(imageSizes[sizeName].width, imageSizes[sizeName].height, {
fit: imageSizes[sizeName].fit
})
.toFile(`${STORAGE_PATH}/${imagePath}`, () => {
return imagePath
})
.toFile(
`${this.configService.get('paths').publicFolder}/${STORAGE_PATH}/${imagePath}`,
() => {
return imagePath
}
)

imagePaths[sizeName] = this.prependStorageUrl(imagePath)
}
Expand All @@ -95,18 +101,20 @@ export class StorageService {

const folder: string = `${kebabize(entity)}/${kebabize(property)}/${dateString}`

mkdirp.sync(`${STORAGE_PATH}/${folder}`)
mkdirp.sync(
`${this.configService.get('paths').publicFolder}/${STORAGE_PATH}/${folder}`
)

return folder
}

/**
* Prepends the storage URL to the given value.
* Prepends the storage absolute URL to the given value.
*
* @param value The value to prepend the storage URL to.
* @returns The value with the storage URL prepended.
*/
prependStorageUrl(value: string): string {
return `${this.configService.get('baseUrl')}/storage/${value}`
return `${this.configService.get('baseUrl')}/${STORAGE_PATH}/${value}`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ describe('StorageService', () => {
expect(filePaths).toBeDefined()
expect(Object.keys(filePaths).length).toBe(2)
expect(Object.keys(filePaths)).toMatchObject(Object.keys(imageSizes))
expect(sharp.prototype.resize).toHaveBeenCalledTimes(2)
expect(sharp.prototype.toFile).toHaveBeenCalledTimes(2)
})

it('should prepend the storage url before the path', () => {
Expand Down
44 changes: 37 additions & 7 deletions packages/core/manifest/src/upload/tests/upload.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,49 @@ describe('UploadController', () => {
})

describe('uploadFile', () => {
it('should return the path of the uploaded file', () => {})
it('should return the path of the uploaded file', () => {
const file = {
buffer: Buffer.from('file content'),
originalname: 'file.txt'
}

it('creates unique file names', () => {})
const entity = 'entity'
const property = 'property'
const path = 'path'

it('should return a 400 error if entity name or property is not provided', () => {})
jest.spyOn(uploadService, 'storeFile').mockReturnValue(path)

it('expect the file to be passed as "file" in a multipart/form-data', () => {})
expect(controller.uploadFile(file, entity, property)).toEqual({ path })

expect(uploadService.storeFile).toHaveBeenCalledWith({
file,
entity,
property
})
})
})
describe('uploadImage', () => {
it('should upload an image', () => {})
it('should upload an image', () => {
const image = {
buffer: Buffer.from('image content'),
originalname: 'image.jpg'
}

const entity = 'entity'
const property = 'property'
const imagePaths = { thumbnail: 'path', medium: 'path' }

jest.spyOn(uploadService, 'storeImage').mockReturnValue(imagePaths)

it('should return a 400 error if entity name or property is not provided', () => {})
expect(controller.uploadImage(image, entity, property)).toEqual(
imagePaths
)

it('expect the image to be passed as "image" in a multipart/form-data', () => {})
expect(uploadService.storeImage).toHaveBeenCalledWith({
image,
entity,
property
})
})
})
})
87 changes: 85 additions & 2 deletions packages/core/manifest/src/upload/tests/upload.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,47 @@ import { Test, TestingModule } from '@nestjs/testing'
import { UploadService } from '../services/upload.service'
import { EntityManifestService } from '../../manifest/services/entity-manifest.service'
import { StorageService } from '../../storage/services/storage.service'
import { PropType, PropertyManifest } from '../../../../types/src'
import { DEFAULT_IMAGE_SIZES } from '../../constants'

describe('UploadService', () => {
let service: UploadService
let storageService: StorageService
let entityManifestService: EntityManifestService

const dummyImageProp: PropertyManifest = {
name: 'avatar',
type: PropType.Image,
options: {
sizes: DEFAULT_IMAGE_SIZES
}
}
const imagePaths = { large: 'imagePaths' }

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UploadService,
{
provide: StorageService,
useValue: {
store: jest.fn()
store: jest.fn(),
storeImage: jest.fn(() => imagePaths)
}
},
{
provide: EntityManifestService,
useValue: {
getEntityManifest: jest.fn()
getEntityManifest: jest.fn(() => ({
properties: [
dummyImageProp,
{
name: 'not-an-image',
type: PropType.String,
options: { required: true }
}
]
}))
}
}
]
Expand All @@ -37,4 +58,66 @@ describe('UploadService', () => {
it('should be defined', () => {
expect(service).toBeDefined()
})

describe('storeFile', () => {
it('should store a file', () => {
const file = {
buffer: Buffer.from('file content'),
originalname: 'file.txt'
}

const entity = 'entity'
const property = 'property'
const path = 'path'

jest.spyOn(storageService, 'store').mockReturnValue(path)

expect(service.storeFile({ file, entity, property })).toEqual(path)

expect(storageService.store).toHaveBeenCalledWith(entity, property, file)
})
})

describe('store image', () => {
it('should store an image', () => {
const image = {
buffer: Buffer.from('image content'),
originalname: 'image.jpg'
}

const entity = 'entity'

const result = service.storeImage({
image,
entity,
property: dummyImageProp.name
})

expect(result).toEqual(imagePaths)

expect(storageService.storeImage).toHaveBeenCalledWith(
entity,
dummyImageProp.name,
image,
dummyImageProp.options.sizes
)
})

it('should fail if property is not an image', () => {
const image = {
buffer: Buffer.from('image content'),
originalname: 'image.jpg'
}

const entity = 'entity'

expect(() =>
service.storeImage({
image,
entity,
property: 'not-an-image'
})
).toThrow()
})
})
})