Compare commits

...

10 Commits

Author SHA1 Message Date
2a5b927988 feat: ajustes visuais que simplificam a interface de envio de arquivos 2025-08-28 02:38:15 -04:00
b2befb71de Remove file upload component files 2025-08-28 02:37:26 -04:00
Jayden Seric
1ed805d979 Update apollo-upload-client to v18. 2023-10-24 01:09:29 +11:00
Jayden Seric
b415d67201 Update app dependencies.
Also includes TypeScript and Prettier fixes.
2023-10-23 23:52:15 +11:00
Jayden Seric
f53d1a2cfd Update supported Node.js versions. 2023-10-23 23:39:48 +11:00
Jayden Seric
a5da903448 Migrate to Apollo Server v4. 2023-01-14 10:59:02 +11:00
Jayden Seric
3a814a90db Better type safety in the app .eslintrc.js module. 2023-01-14 10:45:43 +11:00
Jayden Seric
fe67000e37 Update app dependencies. 2023-01-14 10:43:36 +11:00
Jayden Seric
e55aa27630 For the API package script dev replace nodemon with node —watch. 2023-01-14 10:07:17 +11:00
Jayden Seric
719e53db57 Update the API dependencies. 2023-01-14 08:20:08 +11:00
19 changed files with 4112 additions and 7212 deletions

3782
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,29 +16,33 @@
"bugs": "https://github.com/jaydenseric/apollo-upload-examples/issues",
"funding": "https://github.com/sponsors/jaydenseric",
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0",
"npm": ">= 7"
"node": "^18.15.0 || >=20.4.0",
"npm": ">=7"
},
"dependencies": {
"apollo-server-koa": "^3.10.2",
"dotenv": "^16.0.2",
"@apollo/server": "^4.3.0",
"@as-integrations/koa": "^0.2.1",
"@koa/cors": "^4.0.0",
"dotenv": "^16.0.3",
"graphql": "^16.6.0",
"graphql-upload": "^16.0.2",
"koa": "^2.13.4",
"koa": "^2.14.1",
"koa-bodyparser": "^4.3.0",
"make-dir": "^3.1.0",
"shortid": "^2.2.16"
},
"devDependencies": {
"@types/koa": "^2.13.5",
"@types/node": "^18.7.14",
"eslint": "^8.23.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"nodemon": "^2.0.19",
"prettier": "^2.7.1",
"typescript": "^4.8.2"
"@types/koa__cors": "^3.3.0",
"@types/koa-bodyparser": "^4.3.10",
"@types/node": "^18.11.18",
"eslint": "^8.31.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"prettier": "^2.8.2",
"typescript": "^4.9.4"
},
"scripts": {
"dev": "nodemon",
"dev": "node --watch -r dotenv/config server.mjs",
"start": "node -r dotenv/config server.mjs",
"eslint": "eslint .",
"prettier": "prettier -c .",

View File

@ -3,8 +3,8 @@
An example GraphQL API using:
- [`koa`](https://npm.im/koa)
- [`apollo-server-koa`](https://npm.im/apollo-server-koa)
- [`graphql-upload`](https://npm.im/graphql-upload)
- [`@as-integrations/koa`](https://npm.im/@as-integrations/koa)
## Installation

View File

@ -1,8 +1,9 @@
// @ts-check
import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql";
import { readdir } from "node:fs/promises";
import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql";
import UPLOAD_DIRECTORY_URL from "../config/UPLOAD_DIRECTORY_URL.mjs";
import FileType from "./FileType.mjs";

View File

@ -1,39 +1,46 @@
// @ts-check
import { ApolloServer } from "apollo-server-koa";
import graphqlUploadKoa from "graphql-upload/graphqlUploadKoa.mjs";
import Koa from "koa";
import makeDir from "make-dir";
import { fileURLToPath } from "node:url";
import { ApolloServer } from "@apollo/server";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { koaMiddleware as apolloServerKoa } from "@as-integrations/koa";
import corsKoa from "@koa/cors";
import graphqlUploadKoa from "graphql-upload/graphqlUploadKoa.mjs";
import http from "http";
import Koa from "koa";
import bodyParserKoa from "koa-bodyparser";
import makeDir from "make-dir";
import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs";
import schema from "./schema/index.mjs";
/** Starts the API server. */
async function startServer() {
// Ensure the upload directory exists.
await makeDir(fileURLToPath(UPLOAD_DIRECTORY_URL));
// Ensure the upload directory exists.
await makeDir(fileURLToPath(UPLOAD_DIRECTORY_URL));
const apolloServer = new ApolloServer({ schema });
const app = new Koa();
const httpServer = http.createServer(app.callback());
const apolloServer = new ApolloServer({
schema,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await apolloServer.start();
await apolloServer.start();
new Koa()
.use(
graphqlUploadKoa({
// Limits here should be stricter than config for surrounding
// infrastructure such as Nginx so errors can be handled elegantly by
// `graphql-upload`.
maxFileSize: 10000000, // 10 MB
maxFiles: 20,
})
)
.use(apolloServer.getMiddleware())
.listen(process.env.PORT, () => {
console.info(
`Serving http://localhost:${process.env.PORT} for ${process.env.NODE_ENV}.`
);
});
}
app.use(corsKoa());
app.use(
graphqlUploadKoa({
// Limits here should be stricter than config for surrounding infrastructure
// such as NGINX so errors can be handled elegantly by `graphql-upload`.
maxFileSize: 10000000, // 10 MB
maxFiles: 20,
})
);
app.use(bodyParserKoa());
app.use(apolloServerKoa(apolloServer));
startServer();
httpServer.listen(process.env.PORT, () => {
console.info(
`Serving http://localhost:${process.env.PORT} for ${process.env.NODE_ENV}.`
);
});

View File

@ -1,6 +1,7 @@
// @ts-check
import { createWriteStream, unlink } from "node:fs";
import shortId from "shortid";
import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs";

View File

@ -4,6 +4,7 @@
const { resolve } = require("path");
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["eslint:recommended", "plugin:react-hooks/recommended"],
env: {

View File

@ -14,6 +14,6 @@ export default function Page({ title, children }) {
Fragment,
null,
h(nextHead.default, null, h("title", null, title)),
children
children,
);
}

View File

@ -1,94 +0,0 @@
// @ts-check
import { gql } from "@apollo/client/core";
import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js";
import { useMutation } from "@apollo/client/react/hooks/useMutation.js";
import ButtonSubmit from "device-agnostic-ui/ButtonSubmit.mjs";
import Code from "device-agnostic-ui/Code.mjs";
import Fieldset from "device-agnostic-ui/Fieldset.mjs";
import Textbox from "device-agnostic-ui/Textbox.mjs";
import { createElement as h, Fragment, useState } from "react";
const SINGLE_UPLOAD_MUTATION = gql`
mutation singleUpload($file: Upload!) {
singleUpload(file: $file) {
id
}
}
`;
/** React component for a uploading a blob. */
export default function UploadBlob() {
const [name, setName] = useState("");
const [content, setContent] = useState("");
const [singleUploadMutation, { loading }] = useMutation(
SINGLE_UPLOAD_MUTATION
);
const apolloClient = useApolloClient();
/**
* @type {import("react").ChangeEventHandler<
* HTMLInputElement | HTMLTextAreaElement
* >}
*/
function onNameChange({ target: { value } }) {
setName(value);
}
/**
* @type {import("react").ChangeEventHandler<
* HTMLInputElement | HTMLTextAreaElement
* >}
*/
function onContentChange({ target: { value } }) {
setContent(value);
}
/** @type {import("react").FormEventHandler<HTMLFormElement>} */
function onSubmit(event) {
event.preventDefault();
singleUploadMutation({
variables: {
file: new File([content], `${name}.txt`, { type: "text/plain" }),
},
}).then(() => {
apolloClient.resetStore();
});
}
return h(
"form",
{ onSubmit },
h(
Fieldset,
{
legend: h(
Fragment,
null,
"File name (without ",
h(Code, null, ".txt"),
")"
),
},
h(Textbox, {
placeholder: "Name",
required: true,
value: name,
onChange: onNameChange,
})
),
h(
Fieldset,
{ legend: "File content" },
h(Textbox, {
type: "textarea",
placeholder: "Content",
required: true,
value: content,
onChange: onContentChange,
})
),
h(ButtonSubmit, { loading }, "Upload")
);
}

View File

@ -1,30 +0,0 @@
// @ts-check
import { gql } from "@apollo/client/core";
import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js";
import { useMutation } from "@apollo/client/react/hooks/useMutation.js";
import { createElement as h } from "react";
const SINGLE_UPLOAD_MUTATION = gql`
mutation singleUpload($file: Upload!) {
singleUpload(file: $file) {
id
}
}
`;
/** React component for a uploading a single file. */
export default function UploadFile() {
const [uploadFileMutation] = useMutation(SINGLE_UPLOAD_MUTATION);
const apolloClient = useApolloClient();
/** @type {import("react").ChangeEventHandler<HTMLInputElement>} */
function onChange({ target: { validity, files } }) {
if (validity.valid && files && files[0])
uploadFileMutation({ variables: { file: files[0] } }).then(() => {
apolloClient.resetStore();
});
}
return h("input", { type: "file", required: true, onChange });
}

View File

@ -1,45 +0,0 @@
// @ts-check
import { gql } from "@apollo/client/core";
import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js";
import { useMutation } from "@apollo/client/react/hooks/useMutation.js";
import { createElement as h } from "react";
const MULTIPLE_UPLOAD_MUTATION = gql`
mutation multipleUpload($files: [Upload!]!) {
multipleUpload(files: $files) {
id
}
}
`;
/**
* @typedef {{
* multipleUpload: {
* id: string,
* },
* }} MultipleUploadMutationData
*/
/** React component for a uploading a file list. */
export default function UploadFileList() {
const [multipleUploadMutation] =
/**
* @type {import("@apollo/client/react/types/types.js").MutationTuple<
* MultipleUploadMutationData,
* { files: FileList }
* >}
*/
(useMutation(MULTIPLE_UPLOAD_MUTATION));
const apolloClient = useApolloClient();
/** @type {import("react").ChangeEventHandler<HTMLInputElement>} */
function onChange({ target: { validity, files } }) {
if (validity.valid && files && files[0])
multipleUploadMutation({ variables: { files } }).then(() => {
apolloClient.resetStore();
});
}
return h("input", { type: "file", multiple: true, required: true, onChange });
}

View File

@ -1,51 +0,0 @@
// @ts-check
import { gql } from "@apollo/client/core";
import { useQuery } from "@apollo/client/react/hooks/useQuery.js";
import Scroll from "device-agnostic-ui/Scroll.mjs";
import Table from "device-agnostic-ui/Table.mjs";
import { createElement as h } from "react";
const UPLOADS_QUERY = gql`
query uploads {
uploads {
id
url
}
}
`;
/**
* @typedef {{
* uploads: Array<{
* id: string,
* url: string
* }>,
* }} UploadsQueryData
*/
/** React component for displaying uploads. */
export default function Uploads() {
const { data: { uploads = [] } = {} } =
/**
* @type {import("@apollo/client/react/types/types.js").QueryResult<
* UploadsQueryData
* >}
*/
(useQuery(UPLOADS_QUERY));
return h(
Scroll,
null,
h(
Table,
null,
h("thead", null, h("tr", null, h("th", null, "Stored file URL"))),
h(
"tbody",
null,
uploads.map(({ id, url }) => h("tr", { key: id }, h("td", null, url)))
)
)
);
}

View File

@ -0,0 +1,38 @@
// @ts-check
import { gql } from "@apollo/client/core";
import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js";
import { useMutation } from "@apollo/client/react/hooks/useMutation.js";
import { createElement as h } from "react";
const CRIAR_DOCUMENTO = gql`
mutation CriarDocumento($arquivo: [Upload!]!, $input: CreateDocumentoInput!) {
criarDocumento(arquivo: $arquivo, input: $input) {
id
}
}
`;
export default function CriarDocumento() {
const [multipleUploadMutation] = useMutation(CRIAR_DOCUMENTO);
const apolloClient = useApolloClient();
/** @type {import("react").ChangeEventHandler<HTMLInputElement>} */
function onChange({ target: { validity, files } }) {
if (validity.valid && files && files[0]) {
multipleUploadMutation({
variables: {
arquivo: Array.from(files),
input: {
modeloId: 26,
fluxoId: 33,
},
},
}).then(() => {
apolloClient.resetStore();
});
}
}
return h("input", { type: "file", multiple: true, required: true, onChange });
}

View File

@ -7,5 +7,6 @@
},
"typeAcquisition": {
"enable": false
}
},
"include": ["**/*", "**/.eslintrc.js"]
}

4877
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,32 +16,38 @@
"bugs": "https://github.com/jaydenseric/apollo-upload-examples/issues",
"funding": "https://github.com/sponsors/jaydenseric",
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0",
"npm": ">= 7"
"node": "^18.15.0 || >=20.4.0",
"npm": ">=7"
},
"browserslist": "Node 14.17 - 15 and Node < 15, Node 16 - 17 and Node < 17, Node >= 18, > 0.5%, not OperaMini all, not dead",
"browserslist": "Node 18.15 - 19 and Node < 19, Node >= 20.4, > 0.5%, not OperaMini all, not IE > 0, not dead",
"dependencies": {
"@apollo/client": "^3.6.9",
"apollo-upload-client": "^17.0.0",
"@apollo/client": "^3.8.6",
"apollo-upload-client": "^18.0.0",
"device-agnostic-ui": "~10.0.0",
"graphql": "^16.6.0",
"next": "^12.2.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"graphql": "^16.8.1",
"next": "^13.5.11",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.18.9",
"@types/node": "^18.7.14",
"@types/react": "^17.0.49",
"@types/react-dom": "^17.0.17",
"@babel/eslint-parser": "^7.22.15",
"@types/eslint": "^8.44.6",
"@types/node": "^20.8.7",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"babel-plugin-graphql-tag": "^3.3.0",
"eslint": "^8.23.0",
"eslint": "^8.52.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"prettier": "^2.7.1",
"stylelint": "^14.11.0",
"stylelint-config-recommended": "^9.0.0",
"typescript": "^4.8.2"
"eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^3.0.3",
"stylelint": "^15.11.0",
"stylelint-config-recommended": "^13.0.0",
"typescript": "^5.2.2"
},
"overrides": {
"next": "^13.5.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"dev": "next dev",

View File

@ -16,20 +16,25 @@ import "device-agnostic-ui/Textbox.css";
import { InMemoryCache } from "@apollo/client/cache/inmemory/inMemoryCache.js";
import { ApolloClient } from "@apollo/client/core/ApolloClient.js";
import { ApolloProvider } from "@apollo/client/react/context/ApolloProvider.js";
import { createUploadLink } from "apollo-upload-client";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
import nextApp from "next/app.js";
import nextHead from "next/head.js";
import { createElement as h, Fragment } from "react";
/**
* Creates an Apollo Client instance.
* @param {{ [key: string]: unknown }} [cache] Apollo Client initial cache.
*/
let token = process.env.API_TOKEN;
const createApolloClient = (cache = {}) =>
new ApolloClient({
ssrMode: typeof window === "undefined",
cache: new InMemoryCache().restore(cache),
link: createUploadLink({ uri: process.env.API_URI }),
link: createUploadLink({
uri: process.env.API_URI,
headers: {
"Apollo-Require-Preflight": "true",
"tenant-id": "imagetech",
authorization: `Bearer ${token}`,
},
}),
});
/**
@ -56,9 +61,9 @@ function App({
}),
h("meta", { name: "color-scheme", content: "light dark" }),
h("meta", { name: "theme-color", content: "white" }),
h("link", { rel: "manifest", href: "/manifest.webmanifest" })
h("link", { rel: "manifest", href: "/manifest.webmanifest" }),
),
h(Component, pageProps)
h(Component, pageProps),
),
});
}
@ -71,7 +76,7 @@ if (typeof window === "undefined")
const [props, { default: ReactDOMServer }, { getMarkupFromTree }] =
await Promise.all([
nextApp.default.getInitialProps(context),
import("react-dom/server.js"),
import("react-dom/server"),
import("@apollo/client/react/ssr/getDataFromTree.js"),
]);
@ -86,7 +91,6 @@ if (typeof window === "undefined")
renderFunction: ReactDOMServer.renderToStaticMarkup,
});
} catch (error) {
// Prevent crash from GraphQL errors.
console.error(error);
}
@ -101,6 +105,8 @@ export default App;
/**
* Next.js app custom props.
* @typedef {object} AppCustomProps
* @prop {{ [key: string]: unknown }} [apolloCache] Apollo Client initial cache.
* @prop {import(
* "@apollo/client/cache/inmemory/types.js"
* ).NormalizedCacheObject} [apolloCache] Apollo Client initial cache.
* @prop {ApolloClient<any>} apolloClient Apollo Client.
*/

View File

@ -1,6 +1,5 @@
// @ts-check
import Code from "device-agnostic-ui/Code.mjs";
import Heading from "device-agnostic-ui/Heading.mjs";
import Margin from "device-agnostic-ui/Margin.mjs";
import { createElement as h } from "react";
@ -8,50 +7,20 @@ import { createElement as h } from "react";
import Header from "../components/Header.mjs";
import Page from "../components/Page.mjs";
import Section from "../components/Section.mjs";
import UploadBlob from "../components/UploadBlob.mjs";
import UploadFile from "../components/UploadFile.mjs";
import UploadFileList from "../components/UploadFileList.mjs";
import Uploads from "../components/Uploads.mjs";
import CriarDocumento from "../components/criarDocumento.mjs";
export default function IndexPage() {
return h(
Page,
{ title: "Apollo upload examples" },
h(
Header,
null,
h(Heading, { level: 1, size: 1 }, "Apollo upload examples")
),
{ title: "GeoDoc - Uploads" },
h(Header, null, h(Heading, { level: 1, size: 1 }, "GeoDoc - Uploads")),
h(
Section,
null,
h(
Header,
null,
h(Heading, { level: 2, size: 2 }, "Upload ", h(Code, null, "FileList"))
),
h(Margin, null, h(UploadFileList))
h(Header, null, h(Heading, { level: 2, size: 2 }, "CriarDocumento ")),
h(Margin, null, h(CriarDocumento)),
),
h(
Section,
null,
h(
Header,
null,
h(Heading, { level: 2, size: 2 }, "Upload ", h(Code, null, "File"))
),
h(Margin, null, h(UploadFile))
),
h(
Section,
null,
h(
Header,
null,
h(Heading, { level: 2, size: 2 }, "Upload ", h(Code, null, "Blob"))
),
h(Margin, null, h(UploadBlob))
),
h(Section, null, h(Header, null, h(Heading, null, "Uploads")), h(Uploads))
h(Section, null),
h(Section, null, h(Header, null, h(Heading, null, "Documentos Recentes"))),
);
}

2187
app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff