diff --git a/@app/client/src/graphql/CreateUploadUrl.graphql b/@app/client/src/graphql/CreateUploadUrl.graphql
new file mode 100644
index 00000000..5c14369e
--- /dev/null
+++ b/@app/client/src/graphql/CreateUploadUrl.graphql
@@ -0,0 +1,5 @@
+mutation CreateUploadUrl($input: CreateUploadUrlInput!) {
+ createUploadUrl(input: $input) {
+ uploadUrl
+ }
+}
diff --git a/@app/client/src/graphql/UpdateUser.graphql b/@app/client/src/graphql/UpdateUser.graphql
index 2f70178f..496cd3f5 100644
--- a/@app/client/src/graphql/UpdateUser.graphql
+++ b/@app/client/src/graphql/UpdateUser.graphql
@@ -5,6 +5,7 @@ mutation UpdateUser($id: UUID!, $patch: UserPatch!) {
id
name
username
+ avatarUrl
}
}
}
diff --git a/@app/client/src/pages/settings/index.tsx b/@app/client/src/pages/settings/index.tsx
index 3c1a1754..4ee79fc0 100644
--- a/@app/client/src/pages/settings/index.tsx
+++ b/@app/client/src/pages/settings/index.tsx
@@ -1,4 +1,9 @@
-import { ErrorAlert, Redirect, SettingsLayout } from "@app/components";
+import {
+ AvatarUpload,
+ ErrorAlert,
+ Redirect,
+ SettingsLayout,
+} from "@app/components";
import {
ProfileSettingsForm_UserFragment,
useSettingsProfileQuery,
@@ -10,7 +15,7 @@ import {
getCodeFromError,
tailFormItemLayout,
} from "@app/lib";
-import { Alert, Button, Form, Input, PageHeader } from "antd";
+import { Alert, Button, Card, Form, Input, PageHeader } from "antd";
import { useForm } from "antd/lib/form/util";
import { ApolloError } from "apollo-client";
import { NextPage } from "next";
@@ -104,64 +109,76 @@ function ProfileSettingsForm({
const code = getCodeFromError(error);
return (
);
}
diff --git a/@app/components/package.json b/@app/components/package.json
index 85d97d63..3ba1b1b8 100644
--- a/@app/components/package.json
+++ b/@app/components/package.json
@@ -11,8 +11,11 @@
"@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.5",
"@app/graphql": "0.0.0",
+ "@app/lib": "0.0.0",
+ "@types/axios": "^0.14.0",
"antd": "^4.2.0",
"apollo-client": "^2.6.8",
+ "axios": "^0.19.2",
"next": "^9.3.6",
"react": "^16.13.1",
"tslib": "^1.11.1"
diff --git a/@app/components/src/AvatarUpload.tsx b/@app/components/src/AvatarUpload.tsx
new file mode 100644
index 00000000..1451dfc8
--- /dev/null
+++ b/@app/components/src/AvatarUpload.tsx
@@ -0,0 +1,158 @@
+import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
+import {
+ ProfileSettingsForm_UserFragment,
+ useCreateUploadUrlMutation,
+ useUpdateUserMutation,
+} from "@app/graphql";
+import { extractError, getExceptionFromError } from "@app/lib";
+import { message, Upload } from "antd";
+import {
+ RcCustomRequestOptions,
+ UploadChangeParam,
+ UploadFile,
+} from "antd/lib/upload/interface";
+import axios from "axios";
+import React, { useState } from "react";
+import slugify from "slugify";
+
+export function getUid(name: string) {
+ const randomHex = () => Math.floor(Math.random() * 16777215).toString(16);
+ const fileNameSlug = slugify(name);
+ return randomHex() + "-" + fileNameSlug;
+}
+
+const ALLOWED_UPLOAD_CONTENT_TYPES = [
+ "image/apng",
+ "image/bmp",
+ "image/gif",
+ "image/jpeg",
+ "image/png",
+ "image/svg+xml",
+ "image/tiff",
+ "image/webp",
+];
+const ALLOWED_UPLOAD_CONTENT_TYPES_STRING = ALLOWED_UPLOAD_CONTENT_TYPES.join(
+ ","
+);
+
+export function AvatarUpload({
+ user,
+}: {
+ user: ProfileSettingsForm_UserFragment;
+}) {
+ const [updateUser] = useUpdateUserMutation();
+
+ const beforeUpload = (file: any) => {
+ const fileName = file.name.split(".")[0];
+ const fileType = file.name.split(".")[1];
+ file.uid = getUid(fileName) + "." + fileType;
+ const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
+ if (!isJpgOrPng) {
+ message.error("You can only upload JPG or PNG images!");
+ file.status = "error";
+ }
+ const isLt3M = file.size / 1024 / 1024 < 3;
+ if (!isLt3M) {
+ message.error("Image must smaller than 3MB!");
+ file.status = "error";
+ }
+ return isJpgOrPng && isLt3M;
+ };
+
+ const [createUploadUrl] = useCreateUploadUrlMutation();
+
+ const [loading, setLoading] = useState(false);
+
+ const customRequest = async (option: RcCustomRequestOptions) => {
+ const { onSuccess, onError, file, onProgress } = option;
+ try {
+ const contentType = file.type;
+ const { data } = await createUploadUrl({
+ variables: {
+ input: {
+ contentType,
+ },
+ },
+ });
+ const uploadUrl = data?.createUploadUrl?.uploadUrl;
+
+ if (!uploadUrl) {
+ throw new Error("Failed to generate upload URL");
+ }
+ const response = await axios.put(uploadUrl, file, {
+ onUploadProgress: (e) => {
+ const progress = Math.round((e.loaded / e.total) * 100);
+ onProgress({ percent: progress }, file);
+ },
+ });
+ if (response.config.url) {
+ await updateUser({
+ variables: {
+ id: user.id,
+ patch: {
+ avatarUrl: response.config.url.split("?")[0],
+ },
+ },
+ });
+ onSuccess(response.config, file);
+ }
+ } catch (e) {
+ console.error(e);
+ onError(e);
+ }
+ };
+
+ const uploadButton = (
+
+ {loading ?
:
}
+
Upload
+
+ );
+
+ const onChange = (info: UploadChangeParam>) => {
+ switch (info.file.status) {
+ case "uploading": {
+ setLoading(true);
+ break;
+ }
+ case "removed":
+ case "success": {
+ setLoading(false);
+ break;
+ }
+ case "error": {
+ const error: any = getExceptionFromError(info.file.error);
+ console.dir(error);
+ message.error(
+ typeof error === "string"
+ ? error
+ : error?.message ??
+ "Unknown error occurred" +
+ (error?.code ? ` (${error.code})` : "")
+ );
+ setLoading(false);
+ break;
+ }
+ }
+ };
+
+ return (
+
+
+ {user.avatarUrl ? (
+
+ ) : (
+ uploadButton
+ )}
+
+
+ );
+}
diff --git a/@app/components/src/SharedLayout.tsx b/@app/components/src/SharedLayout.tsx
index 0d07ac8c..fca5d7cd 100644
--- a/@app/components/src/SharedLayout.tsx
+++ b/@app/components/src/SharedLayout.tsx
@@ -18,6 +18,7 @@ import { useCallback } from "react";
import { ErrorAlert, H3, StandardWidth, Warn } from ".";
import { Redirect } from "./Redirect";
+import { UserAvatar } from "./UserAvatar";
const { Header, Content, Footer } = Layout;
const { Text } = Typography;
@@ -243,9 +244,7 @@ export function SharedLayout({
data-cy="layout-dropdown-user"
style={{ whiteSpace: "nowrap" }}
>
-
- {(data.currentUser.name && data.currentUser.name[0]) || "?"}
-
+
{data.currentUser.name}
diff --git a/@app/components/src/UserAvatar.tsx b/@app/components/src/UserAvatar.tsx
new file mode 100644
index 00000000..b5f19ca4
--- /dev/null
+++ b/@app/components/src/UserAvatar.tsx
@@ -0,0 +1,16 @@
+import { Avatar } from "antd";
+import React, { FC } from "react";
+
+export const UserAvatar: FC<{
+ user: {
+ name?: string | null;
+ avatarUrl?: string | null;
+ };
+}> = (props) => {
+ const { name, avatarUrl } = props.user;
+ if (avatarUrl) {
+ return ;
+ } else {
+ return {(name && name[0]) || "?"};
+ }
+};
diff --git a/@app/components/src/index.tsx b/@app/components/src/index.tsx
index 5dafc0b5..3a16507b 100644
--- a/@app/components/src/index.tsx
+++ b/@app/components/src/index.tsx
@@ -1,3 +1,4 @@
+export * from "./AvatarUpload";
export * from "./ButtonLink";
export * from "./ErrorAlert";
export * from "./ErrorOccurred";
@@ -11,5 +12,6 @@ export * from "./SocialLoginOptions";
export * from "./SpinPadded";
export * from "./StandardWidth";
export * from "./Text";
+export * from "./UserAvatar";
export * from "./Warn";
export * from "./organizationHooks";
diff --git a/@app/server/package.json b/@app/server/package.json
index e922bd70..17f3d91f 100644
--- a/@app/server/package.json
+++ b/@app/server/package.json
@@ -24,6 +24,8 @@
"@types/passport-github2": "^1.2.4",
"@types/pg": "^7.14.1",
"@types/redis": "^2.8.18",
+ "@types/uuid": "^7.0.3",
+ "aws-sdk": "^2.668.0",
"body-parser": "^1.19.0",
"chalk": "^4.0.0",
"connect-pg-simple": "^6.1.0",
@@ -44,7 +46,8 @@
"postgraphile": "^4.7.0",
"redis": "^3.0.2",
"source-map-support": "^0.5.13",
- "tslib": "^1.11.1"
+ "tslib": "^1.11.1",
+ "uuid": "^8.0.0"
},
"devDependencies": {
"@types/node": "^13.13.4",
diff --git a/@app/server/src/middleware/installPostGraphile.ts b/@app/server/src/middleware/installPostGraphile.ts
index 705d899a..54ba5940 100644
--- a/@app/server/src/middleware/installPostGraphile.ts
+++ b/@app/server/src/middleware/installPostGraphile.ts
@@ -15,6 +15,7 @@ import {
import { makePgSmartTagsFromFilePlugin } from "postgraphile/plugins";
import { getHttpServer, getWebsocketMiddlewares } from "../app";
+import CreateUploadUrlPlugin from "../plugins/CreateUploadUrlPlugin";
import OrdersPlugin from "../plugins/Orders";
import PassportLoginPlugin from "../plugins/PassportLoginPlugin";
import PrimaryKeyMutationsOnlyPlugin from "../plugins/PrimaryKeyMutationsOnlyPlugin";
@@ -183,6 +184,9 @@ export function getPostGraphileOptions({
// Adds custom orders to our GraphQL schema
OrdersPlugin,
+
+ // Allows API clients to fetch a pre-signed URL for uploading files
+ CreateUploadUrlPlugin,
],
/*
diff --git a/@app/server/src/plugins/CreateUploadUrlPlugin.ts b/@app/server/src/plugins/CreateUploadUrlPlugin.ts
new file mode 100644
index 00000000..248730f7
--- /dev/null
+++ b/@app/server/src/plugins/CreateUploadUrlPlugin.ts
@@ -0,0 +1,170 @@
+import { awsRegion } from "@app/config";
+import * as aws from "aws-sdk";
+import { gql, makeExtendSchemaPlugin } from "graphile-utils";
+import { Pool } from "pg";
+import { v4 as uuidv4 } from "uuid";
+
+import { OurGraphQLContext } from "../middleware/installPostGraphile";
+
+const uploadBucket = process.env.S3_UPLOADS_BUCKET;
+
+const s3 = new aws.S3({
+ region: awsRegion,
+ signatureVersion: "v4",
+});
+
+interface CreateUploadUrlInput {
+ clientMutationId?: string;
+ contentType: string;
+}
+
+/** The minimal set of information that this plugin needs to know about users. */
+interface User {
+ id: string;
+ isVerified: boolean;
+}
+
+async function getCurrentUser(pool: Pool): Promise {
+ await pool.query("SAVEPOINT");
+ try {
+ const {
+ rows: [row],
+ } = await pool.query(
+ "select id, is_verified from app_public.users where id = app_public.current_user_id()"
+ );
+ if (!row) {
+ return null;
+ }
+ return {
+ id: row.id,
+ isVerified: row.is_verified,
+ };
+ } catch (err) {
+ await pool.query("ROLLBACK TO SAVEPOINT");
+ throw err;
+ } finally {
+ await pool.query("RELEASE SAVEPOINT");
+ }
+}
+/** The set of content types that we allow users to upload.*/
+const ALLOWED_UPLOAD_CONTENT_TYPES = [
+ "image/apng",
+ "image/bmp",
+ "image/gif",
+ "image/jpeg",
+ "image/png",
+ "image/svg+xml",
+ "image/tiff",
+ "image/webp",
+];
+
+const CreateUploadUrlPlugin = makeExtendSchemaPlugin(() => ({
+ typeDefs: gql`
+ """
+ All input for the \`createUploadUrl\` mutation.
+ """
+ input CreateUploadUrlInput @scope(isMutationInput: true) {
+ """
+ An arbitrary string value with no semantic meaning. Will be included in the
+ payload verbatim. May be used to track mutations by the client.
+ """
+ clientMutationId: String
+
+ """
+ You must provide the content type (or MIME type) of the content you intend
+ to upload. For further information about content types, see
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
+ """
+ contentType: String!
+ }
+
+ """
+ The output of our \`createUploadUrl\` mutation.
+ """
+ type CreateUploadUrlPayload @scope(isMutationPayload: true) {
+ """
+ The exact same \`clientMutationId\` that was provided in the mutation input,
+ unchanged and unused. May be used by a client to track mutations.
+ """
+ clientMutationId: String
+
+ """
+ Upload content to this signed URL.
+ """
+ uploadUrl: String!
+ }
+
+ extend type Mutation {
+ """
+ Get a signed URL for uploading files. It will expire in 5 minutes.
+ """
+ createUploadUrl(
+ """
+ The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields.
+ """
+ input: CreateUploadUrlInput!
+ ): CreateUploadUrlPayload
+ }
+ `,
+ resolvers: {
+ Mutation: {
+ async createUploadUrl(
+ _query,
+ args: { input: CreateUploadUrlInput },
+ context: OurGraphQLContext,
+ _resolveInfo
+ ) {
+ if (!uploadBucket) {
+ const err = new Error(
+ "Server misconfigured: missing `S3_UPLOADS_BUCKET` envvar"
+ );
+ // @ts-ignore
+ err.code = "MSCFG";
+ throw err;
+ }
+
+ const user = await getCurrentUser(context.rootPgPool);
+
+ if (!user) {
+ const err = new Error("Login required");
+ // @ts-ignore
+ err.code = "LOGIN";
+ throw err;
+ }
+
+ if (!user.isVerified) {
+ const err = new Error("Only verified users may upload files");
+ // @ts-ignore
+ err.code = "DNIED";
+ throw err;
+ }
+
+ const {
+ input: { contentType, clientMutationId },
+ } = args;
+ if (!ALLOWED_UPLOAD_CONTENT_TYPES.includes(contentType)) {
+ throw new Error(
+ `Not allowed to upload that type; allowed types include: '${ALLOWED_UPLOAD_CONTENT_TYPES.join(
+ "', '"
+ )}'`
+ );
+ }
+ const params = {
+ Bucket: uploadBucket,
+ ContentType: contentType,
+ // randomly generated filename, nested under username directory
+ Key: `avatars/${user.id}/${uuidv4()}`,
+ Expires: 300, // signed URL will expire in 5 minutes
+ ACL: "public-read", // uploaded file will be publicly readable
+ };
+ const signedUrl = await s3.getSignedUrlPromise("putObject", params);
+ return {
+ clientMutationId,
+ uploadUrl: signedUrl,
+ };
+ },
+ },
+ },
+}));
+
+export default CreateUploadUrlPlugin;
diff --git a/@app/server/src/utils/handleErrors.ts b/@app/server/src/utils/handleErrors.ts
index a80213eb..e0d85650 100644
--- a/@app/server/src/utils/handleErrors.ts
+++ b/@app/server/src/utils/handleErrors.ts
@@ -58,6 +58,10 @@ export const ERROR_MESSAGE_OVERRIDES: { [code: string]: typeof pluck } = {
fields: conflictFieldsFromError(err),
code: "NUNIQ",
}),
+ MSCFG: (err) => ({
+ ...pluck(err),
+ message: err.message,
+ }),
};
function conflictFieldsFromError(err: any) {
diff --git a/data/schema.graphql b/data/schema.graphql
index ff39193c..df9ec8a6 100644
--- a/data/schema.graphql
+++ b/data/schema.graphql
@@ -106,6 +106,39 @@ type CreateOrganizationPayload {
query: Query
}
+"""All input for the `createUploadUrl` mutation."""
+input CreateUploadUrlInput {
+ """
+ An arbitrary string value with no semantic meaning. Will be included in the
+ payload verbatim. May be used to track mutations by the client.
+ """
+ clientMutationId: String
+
+ """
+ You must provide the content type (or MIME type) of the content you intend
+ to upload. For further information about content types, see
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
+ """
+ contentType: String!
+}
+
+"""The output of our `createUploadUrl` mutation."""
+type CreateUploadUrlPayload {
+ """
+ The exact same `clientMutationId` that was provided in the mutation input,
+ unchanged and unused. May be used by a client to track mutations.
+ """
+ clientMutationId: String
+
+ """
+ Our root query field type. Allows us to run any query from our mutation payload.
+ """
+ query: Query
+
+ """Upload content to this signed URL."""
+ uploadUrl: String!
+}
+
"""All input for the create `UserEmail` mutation."""
input CreateUserEmailInput {
"""
@@ -391,6 +424,14 @@ type Mutation {
input: CreateOrganizationInput!
): CreateOrganizationPayload
+ """Get a signed URL for uploading files. It will expire in 5 minutes."""
+ createUploadUrl(
+ """
+ The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields.
+ """
+ input: CreateUploadUrlInput!
+ ): CreateUploadUrlPayload
+
"""Creates a single `UserEmail`."""
createUserEmail(
"""
diff --git a/docs/error_codes.md b/docs/error_codes.md
index 83898e4b..8c55c58a 100644
--- a/docs/error_codes.md
+++ b/docs/error_codes.md
@@ -33,6 +33,7 @@ Rewritten, the above rules state:
- DNIED: permission denied
- NUNIQ: not unique (from PostgreSQL 23505)
- NTFND: not found
+- MSCFG: server misconfigured
## Registration
diff --git a/yarn.lock b/yarn.lock
index 6fead50e..3afa9013 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2675,6 +2675,13 @@
dependencies:
"@types/node" "*"
+"@types/axios@^0.14.0":
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
+ integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
+ dependencies:
+ axios "*"
+
"@types/babel__core@^7.1.7":
version "7.1.7"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89"
@@ -3174,6 +3181,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+"@types/uuid@^7.0.3":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.3.tgz#45cd03e98e758f8581c79c535afbd4fc27ba7ac8"
+ integrity sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==
+
"@types/ws@^6.0.1":
version "6.0.4"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
@@ -4089,6 +4101,13 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+axios@*, axios@^0.19.2:
+ version "0.19.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+ integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+ dependencies:
+ follow-redirects "1.5.10"
+
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -5930,7 +5949,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@3.1.0:
+debug@3.1.0, debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
@@ -7305,6 +7324,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+follow-redirects@1.5.10:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+ integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+ dependencies:
+ debug "=3.1.0"
+
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -11962,9 +11988,9 @@ performance-now@^2.1.0:
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pg-connection-string@0.1.3, pg-connection-string@2.x, pg-connection-string@^2.0.0, pg-connection-string@^2.1.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.2.1.tgz#fd22870c4cdff66f085a8759ee68cde8e9ca0cbf"
- integrity sha512-WnIjIJR575VyX+jRqpSt5PqbIzr3+1D793CkBbQWvpiNTGuHffZEO9+VrwAw0XSVfvxea7QiplHINGahWe9rjg==
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.2.2.tgz#fdc2fb2084e655f0b3e6e50c69330f6932df452e"
+ integrity sha512-+hel4DGuSZCjCZwglAuyi+XlodHnKmrbyTw0hVWlmGN2o4AfJDkDo5obAFzblS5M5PFBMx0uDt5Y1QjlNC+tqg==
pg-int8@1.0.1:
version "1.0.1"
@@ -15190,6 +15216,17 @@ ts-log@2.1.4:
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.1.4.tgz#063c5ad1cbab5d49d258d18015963489fb6fb59a"
integrity sha512-P1EJSoyV+N3bR/IWFeAqXzKPZwHpnLY6j7j58mAvewHRipo+BQM2Y1f9Y9BjEQznKwgqqZm7H8iuixmssU7tYQ==
+ts-node@^8.10.1:
+ version "8.10.1"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3"
+ integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw==
+ dependencies:
+ arg "^4.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ source-map-support "^0.5.17"
+ yn "3.1.1"
+
ts-node@^8.9.1:
version "8.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
@@ -15623,6 +15660,11 @@ uuid@^7.0.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
+uuid@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
+ integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
+
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"