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

View File

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

View File

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

View File

@ -1,39 +1,46 @@
// @ts-check // @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 { 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 UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs";
import schema from "./schema/index.mjs"; import schema from "./schema/index.mjs";
/** Starts the API server. */ // Ensure the upload directory exists.
async function startServer() { 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() app.use(corsKoa());
.use( app.use(
graphqlUploadKoa({ graphqlUploadKoa({
// Limits here should be stricter than config for surrounding // Limits here should be stricter than config for surrounding infrastructure
// infrastructure such as Nginx so errors can be handled elegantly by // such as NGINX so errors can be handled elegantly by `graphql-upload`.
// `graphql-upload`. maxFileSize: 10000000, // 10 MB
maxFileSize: 10000000, // 10 MB maxFiles: 20,
maxFiles: 20, })
}) );
) app.use(bodyParserKoa());
.use(apolloServer.getMiddleware()) app.use(apolloServerKoa(apolloServer));
.listen(process.env.PORT, () => {
console.info(
`Serving http://localhost:${process.env.PORT} for ${process.env.NODE_ENV}.`
);
});
}
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 // @ts-check
import { createWriteStream, unlink } from "node:fs"; import { createWriteStream, unlink } from "node:fs";
import shortId from "shortid"; import shortId from "shortid";
import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs"; import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs";

View File

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

View File

@ -14,6 +14,6 @@ export default function Page({ title, children }) {
Fragment, Fragment,
null, null,
h(nextHead.default, null, h("title", null, title)), 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": { "typeAcquisition": {
"enable": false "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", "bugs": "https://github.com/jaydenseric/apollo-upload-examples/issues",
"funding": "https://github.com/sponsors/jaydenseric", "funding": "https://github.com/sponsors/jaydenseric",
"engines": { "engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0", "node": "^18.15.0 || >=20.4.0",
"npm": ">= 7" "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": { "dependencies": {
"@apollo/client": "^3.6.9", "@apollo/client": "^3.8.6",
"apollo-upload-client": "^17.0.0", "apollo-upload-client": "^18.0.0",
"device-agnostic-ui": "~10.0.0", "device-agnostic-ui": "~10.0.0",
"graphql": "^16.6.0", "graphql": "^16.8.1",
"next": "^12.2.5", "next": "^13.5.11",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.18.9", "@babel/eslint-parser": "^7.22.15",
"@types/node": "^18.7.14", "@types/eslint": "^8.44.6",
"@types/react": "^17.0.49", "@types/node": "^20.8.7",
"@types/react-dom": "^17.0.17", "@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"babel-plugin-graphql-tag": "^3.3.0", "babel-plugin-graphql-tag": "^3.3.0",
"eslint": "^8.23.0", "eslint": "^8.52.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.7.1", "prettier": "^3.0.3",
"stylelint": "^14.11.0", "stylelint": "^15.11.0",
"stylelint-config-recommended": "^9.0.0", "stylelint-config-recommended": "^13.0.0",
"typescript": "^4.8.2" "typescript": "^5.2.2"
},
"overrides": {
"next": "^13.5.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}, },
"scripts": { "scripts": {
"dev": "next dev", "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 { InMemoryCache } from "@apollo/client/cache/inmemory/inMemoryCache.js";
import { ApolloClient } from "@apollo/client/core/ApolloClient.js"; import { ApolloClient } from "@apollo/client/core/ApolloClient.js";
import { ApolloProvider } from "@apollo/client/react/context/ApolloProvider.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 nextApp from "next/app.js";
import nextHead from "next/head.js"; import nextHead from "next/head.js";
import { createElement as h, Fragment } from "react"; import { createElement as h, Fragment } from "react";
/** let token = process.env.API_TOKEN;
* Creates an Apollo Client instance.
* @param {{ [key: string]: unknown }} [cache] Apollo Client initial cache.
*/
const createApolloClient = (cache = {}) => const createApolloClient = (cache = {}) =>
new ApolloClient({ new ApolloClient({
ssrMode: typeof window === "undefined", ssrMode: typeof window === "undefined",
cache: new InMemoryCache().restore(cache), 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: "color-scheme", content: "light dark" }),
h("meta", { name: "theme-color", content: "white" }), 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 }] = const [props, { default: ReactDOMServer }, { getMarkupFromTree }] =
await Promise.all([ await Promise.all([
nextApp.default.getInitialProps(context), nextApp.default.getInitialProps(context),
import("react-dom/server.js"), import("react-dom/server"),
import("@apollo/client/react/ssr/getDataFromTree.js"), import("@apollo/client/react/ssr/getDataFromTree.js"),
]); ]);
@ -86,7 +91,6 @@ if (typeof window === "undefined")
renderFunction: ReactDOMServer.renderToStaticMarkup, renderFunction: ReactDOMServer.renderToStaticMarkup,
}); });
} catch (error) { } catch (error) {
// Prevent crash from GraphQL errors.
console.error(error); console.error(error);
} }
@ -101,6 +105,8 @@ export default App;
/** /**
* Next.js app custom props. * Next.js app custom props.
* @typedef {object} AppCustomProps * @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. * @prop {ApolloClient<any>} apolloClient Apollo Client.
*/ */

View File

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

2187
app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff