Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This was strictly prohibited in old code-gen, and you could only use it with `R.files()`, which restricted using other post body values. Until now, no behavior was specified for current code-gen. This adds support for using 'T.file()' with `R.body()`. It restricts other values to only use basic types, so we don't define behavior for array or object types in multipart/form-data handling.
- Loading branch information
Showing
19 changed files
with
741 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../examples/file-handling/README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
NODE_ENV=development | ||
APP_NAME=file-handling | ||
|
||
POSTGRES_HOST=127.0.0.1:5432 | ||
POSTGRES_USER=postgres | ||
POSTGRES_PASSWORD=postgres |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# CRUD basics | ||
|
||
This project is created using the | ||
[file-handling](https://github.com/compasjs/compas/tree/main/examples/file-handling) | ||
template via [create-compas](https://www.npmjs.com/package/create-compas). | ||
|
||
```shell | ||
# Via NPM | ||
npx create-compas@latest --template file-handling | ||
|
||
# Or with Yarn | ||
yarn create compas --template file-handling | ||
``` | ||
|
||
## Getting started | ||
|
||
- Start up the development Postgres and Minio instances | ||
- `compas docker up` | ||
- Apply the Postgres migrations | ||
- `compas migrate` | ||
- Regenerate router, validators, types, sql, etc. | ||
- `compas run generate` | ||
- Run the tests | ||
- `compas test --serial` | ||
- Start the API | ||
- `compas run api` |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | ||
|
||
|
||
CREATE TABLE "file" | ||
( | ||
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), | ||
"contentLength" int NOT NULL, | ||
"bucketName" varchar NOT NULL, | ||
"contentType" varchar NOT NULL, | ||
"name" varchar NOT NULL, | ||
"meta" jsonb NOT NULL, | ||
"createdAt" timestamptz NOT NULL DEFAULT now(), | ||
"updatedAt" timestamptz NOT NULL DEFAULT now() | ||
); | ||
|
||
CREATE INDEX "fileBucketNameIdx" ON "file" ("bucketName"); | ||
|
||
CREATE TABLE "job" | ||
( | ||
"id" bigserial PRIMARY KEY NOT NULL, | ||
"isComplete" boolean NOT NULL, | ||
"priority" int NOT NULL, | ||
"retryCount" int NOT NULL DEFAULT 0, | ||
"name" varchar NOT NULL, | ||
"scheduledAt" timestamptz NOT NULL DEFAULT now(), | ||
"data" jsonb NOT NULL, | ||
"handlerTimeout" int NULL, | ||
"createdAt" timestamptz NOT NULL DEFAULT now(), | ||
"updatedAt" timestamptz NOT NULL DEFAULT now() | ||
); | ||
|
||
CREATE INDEX "jobIsCompleteScheduledAtIdx" ON "job" ("isComplete", "scheduledAt"); | ||
CREATE INDEX "jobNameIdx" ON "job" ("name"); | ||
CREATE INDEX "jobScheduledAtIdx" ON "job" ("scheduledAt"); | ||
CREATE INDEX IF NOT EXISTS "jobIsCompleteUpdatedAt" ON "job" ("isComplete", "updatedAt") WHERE "isComplete" IS TRUE; | ||
|
||
|
||
CREATE TABLE "post" | ||
( | ||
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), | ||
"contents" varchar NOT NULL, | ||
"title" varchar NOT NULL, | ||
"headerImage" uuid NULL, | ||
"createdAt" timestamptz NOT NULL DEFAULT now(), | ||
"updatedAt" timestamptz NOT NULL DEFAULT now(), | ||
constraint "postHeaderImageFk" foreign key ("headerImage") references "file" ("id") ON DELETE SET NULL | ||
); | ||
|
||
CREATE INDEX "postDatesIdx" ON "post" ("createdAt", "updatedAt"); | ||
CREATE INDEX "postTitleIdx" ON "post" ("title"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"private": true, | ||
"version": "0.0.1", | ||
"type": "module", | ||
"dependencies": { | ||
"@compas/cli": "*", | ||
"@compas/server": "*", | ||
"@compas/stdlib": "*", | ||
"@compas/store": "*" | ||
}, | ||
"devDependencies": { | ||
"@compas/code-gen": "*", | ||
"@compas/eslint-plugin": "*" | ||
}, | ||
"prettier": "@compas/eslint-plugin/prettierrc", | ||
"exampleMetadata": { | ||
"generating": "compas run generate", | ||
"testing": [ | ||
"compas migrate", | ||
"compas test --serial" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { environment, isProduction, isStaging, mainFn } from "@compas/stdlib"; | ||
import { app, injectServices } from "../src/services.js"; | ||
|
||
mainFn(import.meta, main); | ||
|
||
async function main(logger) { | ||
await injectServices(); | ||
|
||
const port = Number(environment.PORT ?? "3001"); | ||
app.listen(port, () => { | ||
logger.info({ | ||
message: "Listening...", | ||
port, | ||
isProduction: isProduction(), | ||
isStaging: isStaging(), | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { TypeCreator, Generator } from "@compas/code-gen"; | ||
import { mainFn } from "@compas/stdlib"; | ||
import { storeGetStructure } from "@compas/store"; | ||
|
||
mainFn(import.meta, main); | ||
|
||
function main() { | ||
const generator = new Generator(); | ||
const T = new TypeCreator("post"); | ||
const R = T.router("/post"); | ||
const Tdatabase = new TypeCreator("database"); | ||
|
||
generator.addStructure(storeGetStructure()); | ||
|
||
generator.add( | ||
Tdatabase.object("post") | ||
.keys({ | ||
title: T.string().min(3).searchable(), | ||
contents: T.string(), | ||
}) | ||
.enableQueries({ | ||
withDates: true, | ||
}) | ||
.relations( | ||
T.oneToOne( | ||
"headerImage", | ||
T.reference("store", "file"), | ||
"postHeaderImage", | ||
).optional(), | ||
), | ||
|
||
R.get("/list", "list").response({ | ||
posts: [ | ||
{ | ||
id: T.uuid(), | ||
title: T.string(), | ||
contents: T.string(), | ||
headerImage: T.reference("store", "fileResponse").optional(), | ||
}, | ||
], | ||
}), | ||
|
||
R.post("/create", "create") | ||
.body({ | ||
title: T.string(), | ||
contents: T.string(), | ||
headerImage: T.file().optional(), | ||
}) | ||
.response({}), | ||
|
||
R.get("/:id/header-image", "headerImage") | ||
.params({ | ||
id: T.uuid(), | ||
}) | ||
.query(T.reference("store", "imageTransformOptions")) | ||
.response(T.file()), | ||
); | ||
|
||
generator.generate({ | ||
targetLanguage: "js", | ||
outputDirectory: "./src/generated", | ||
generators: { | ||
database: { | ||
target: { | ||
dialect: "postgres", | ||
includeDDL: true, | ||
}, | ||
}, | ||
apiClient: { | ||
target: { | ||
targetRuntime: "node.js", | ||
library: "fetch", | ||
}, | ||
responseValidation: { | ||
looseObjectValidation: false, | ||
}, | ||
}, | ||
types: { | ||
declareGlobalTypes: true, | ||
}, | ||
router: { | ||
target: { | ||
library: "koa", | ||
}, | ||
}, | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { createBodyParser, getApp } from "@compas/server"; | ||
import { | ||
AppError, | ||
environment, | ||
isNil, | ||
isProduction, | ||
uuid, | ||
} from "@compas/stdlib"; | ||
import { | ||
createTestPostgresDatabase, | ||
fileCreateOrUpdate, | ||
fileFormatMetadata, | ||
fileSendTransformedImageResponse, | ||
newPostgresConnection, | ||
objectStorageCreateClient, | ||
objectStorageEnsureBucket, | ||
objectStorageGetDevelopmentConfig, | ||
} from "@compas/store"; | ||
import { queries } from "./generated/common/database.js"; | ||
import { router } from "./generated/common/router.js"; | ||
import { queryPost } from "./generated/database/post.js"; | ||
|
||
export let app = undefined; | ||
|
||
export let sql = undefined; | ||
|
||
export let s3Client = undefined; | ||
|
||
export let bucketName = undefined; | ||
|
||
export async function injectServices() { | ||
app = getApp({}); | ||
sql = await newPostgresConnection({ max: 10 }); | ||
|
||
s3Client = objectStorageCreateClient( | ||
isProduction() ? {} : objectStorageGetDevelopmentConfig(), | ||
); | ||
bucketName = environment.APP_NAME; | ||
await objectStorageEnsureBucket(s3Client, { | ||
bucketName, | ||
locationConstraint: "eu-central-1", | ||
}); | ||
|
||
await loadRoutes(); | ||
|
||
app.use( | ||
router( | ||
createBodyParser({ | ||
multipart: true, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
export async function injectTestServices() { | ||
app = getApp({}); | ||
sql = await createTestPostgresDatabase(); | ||
|
||
s3Client = objectStorageCreateClient(objectStorageGetDevelopmentConfig()); | ||
bucketName = uuid(); | ||
await objectStorageEnsureBucket(s3Client, { | ||
bucketName, | ||
locationConstraint: "eu-central-1", | ||
}); | ||
|
||
await loadRoutes(); | ||
|
||
app.use( | ||
router( | ||
createBodyParser({ | ||
multipart: true, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
/** | ||
* Register all routes | ||
*/ | ||
async function loadRoutes() { | ||
const { postHandlers } = await import("./generated/post/controller.js"); | ||
|
||
postHandlers.list = async (ctx) => { | ||
const posts = await queryPost({ | ||
headerImage: {}, | ||
}).exec(sql); | ||
|
||
ctx.body = { | ||
posts: posts.map((it) => ({ | ||
id: it.id, | ||
title: it.title, | ||
contents: it.contents, | ||
headerImage: it.headerImage | ||
? fileFormatMetadata(it.headerImage, { | ||
url: `http://${ctx.request.host}/post/${it.id}/header-image`, | ||
}) | ||
: undefined, | ||
})), | ||
}; | ||
}; | ||
|
||
postHandlers.create = async (ctx) => { | ||
const { headerImage, ...props } = ctx.validatedBody; | ||
|
||
if (headerImage) { | ||
const file = await fileCreateOrUpdate( | ||
sql, | ||
s3Client, | ||
{ | ||
bucketName, | ||
allowedContentTypes: ["image/jpg", "image/png", "image/jpeg"], | ||
}, | ||
{ | ||
name: headerImage.originalFilename, | ||
}, | ||
headerImage.filepath, | ||
); | ||
|
||
props.headerImage = file.id; | ||
} | ||
|
||
await queries.postInsert(sql, props); | ||
|
||
ctx.body = {}; | ||
}; | ||
|
||
postHandlers.headerImage = async (ctx) => { | ||
const [post] = await queryPost({ | ||
where: { | ||
id: ctx.validatedParams.id, | ||
}, | ||
headerImage: {}, | ||
}).exec(sql); | ||
|
||
if (isNil(post?.headerImage)) { | ||
throw AppError.validationError(`post.headerImage.unknown`); | ||
} | ||
|
||
await fileSendTransformedImageResponse( | ||
sql, | ||
s3Client, | ||
ctx, | ||
post.headerImage, | ||
); | ||
}; | ||
} |
Oops, something went wrong.