add next-prisma-trpc example (#198)

This commit is contained in:
Faraz Patankar
2021-09-27 16:56:34 +04:00
committed by GitHub
parent 7c7b2b8522
commit 419f2c4522
32 changed files with 19046 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
{
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
"extends": [
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
"sourceType": "module" // Allows for the use of imports
},
"rules": {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/no-explicit-any": "off"
},
// "overrides": [
// {
// "files": [],
// "rules": {
// "@typescript-eslint/no-unused-vars": "off"
// }
// }
// ],
"settings": {
"react": {
"version": "detect"
}
}
}

View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: KATT

View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
open-pull-requests-limit: 2

View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main, 0.x ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '27 0 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'typescript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -0,0 +1,38 @@
name: E2E-testing
on: [push]
jobs:
e2e:
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/trpcdb
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['14.x']
os: [ubuntu-latest]
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: trpcdb
ports:
- 5432:5432
steps:
- name: Checkout repo
uses: actions/checkout@v2
# - name: Install deps and build (with cache)
# uses: bahmutov/npm-install@v1
- run: yarn install
- name: Next.js cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-${{ runner.node }}--nextjs
- run: yarn playwright install-deps
- run: yarn lint
- run: yarn build
- run: yarn test-start
- run: yarn test-dev

38
examples/next-prisma-trpc/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
*.db
*.db-journal
prisma/_sqlite/migrations

View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"prisma.prisma"
]
}

View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -0,0 +1,31 @@
---
title: NextJS Prisma tRPC
description: A NextJS app using Prisma with tRPC
tags:
- next
- react
- prisma
- trpc
- postgresql
- typescript
---
# NextJS Prisma tRPC
This is an example [NextJS](https://nextjs.org/) app that uses [Prisma](https://www.prisma.io/) with [tRPC](https://trpc.io/).
## ✨ Features
- NextJS
- Prisma
- PostgreSQL
- E2E typesafety with [tRPC](https://trpc.io)
## 💁‍♀️ How to use
- Click the `Deploy on Railway` button
## 📝 Notes
- [tRPC docs with NextJS](https://trpc.io/docs/nextjs)
- [Comparison with BlitzJS](https://trpc.io/docs/further-reading#differences-to-blitzjs)

View File

@@ -0,0 +1,8 @@
// https://github.com/playwright-community/jest-playwright/#configuration
module.exports = {
browsers: ['chromium', 'firefox', 'webkit'],
exitOnPageError: false, // GitHub currently throws errors
launchOptions: {
headless: true,
},
};

View File

@@ -0,0 +1,7 @@
module.exports = {
verbose: true,
preset: 'jest-playwright-preset',
transform: {
'^.+\\.ts$': 'ts-jest',
},
};

View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,4 @@
/**
* @link https://nextjs.org/docs/api-reference/next.config.js/introduction
*/
module.exports = {};

18204
examples/next-prisma-trpc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
{
"name": "@examples/next-starter",
"version": "9.8.0",
"private": true,
"scripts": {
"build:1-generate": "prisma generate",
"build:2-migrate": "prisma migrate deploy",
"build:3-build": "next build",
"build": "run-s build:*",
"dev": "yarn dx:next",
"dx:next": "yarn migrate-dev && yarn generate && next dev",
"dx:studio": "yarn studio",
"dx": "run-p dx:* --print-label",
"dev-nuke": "rm -rf prisma/*.db**",
"generate": "prisma generate",
"migrate-dev": "prisma migrate dev",
"migrate": "prisma migrate deploy",
"start": "next start",
"studio": "prisma studio",
"lint": "eslint src",
"lint-fix": "yarn lint --fix",
"test-dev": "start-server-and-test dev 3000 test",
"test-start": "start-server-and-test start 3000 test",
"test": "jest"
},
"prettier": {
"printWidth": 80,
"trailingComma": "all",
"singleQuote": true
},
"dependencies": {
"@prisma/client": "^3.0.1",
"@trpc/client": "^9.8.0",
"@trpc/next": "^9.8.0",
"@trpc/react": "^9.8.0",
"@trpc/server": "^9.8.0",
"clsx": "^1.1.1",
"next": "^11.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-query": "^3.6.0",
"start-server-and-test": "^1.12.0",
"superjson": "^1.7.4",
"zod": "^3.0.0"
},
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/node": "^16.0.0",
"@types/react": "^17.0.20",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.26.0",
"eslint": "^7.32.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^27.1.0",
"jest-playwright": "^0.0.1",
"jest-playwright-preset": "^1.4.5",
"npm-run-all": "^4.1.5",
"playwright": "^1.14.1",
"prettier": "^2.3.2",
"prisma": "^3.0.1",
"ts-jest": "^27.0.5",
"typescript": "4.4.3"
},
"publishConfig": {
"access": "restricted"
}
}

View File

@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "Post" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"text" TEXT NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -0,0 +1,22 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id String @id @default(uuid())
title String
text String
// To return `Date`s intact through the API we need to add data transformers
// https://trpc.io/docs/data-transformers
// createdAt DateTime @unique @default(now())
// updatedAt DateTime @unique @default(now())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
{
"template": "next"
}

View File

@@ -0,0 +1,85 @@
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next';
import { AppType } from 'next/dist/shared/lib/utils';
import { AppRouter } from 'server/routers/_app';
import superjson from 'superjson';
const MyApp: AppType = ({ Component, pageProps }) => {
return (
<>
<Component {...pageProps} />
</>
);
};
function getBaseUrl() {
if (process.browser) {
return '';
}
// // reference for vercel.com
// if (process.env.VERCEL_URL) {
// return `https://${process.env.VERCEL_URL}`;
// }
// // reference for render.com
// if (process.env.RENDER_INTERNAL_HOSTNAME) {
// return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
// }
// assume localhost
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export default withTRPC<AppRouter>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config() {
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
return {
/**
* @link https://trpc.io/docs/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
/**
* @link https://trpc.io/docs/data-transformers
*/
transformer: superjson,
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: true,
/**
* Set headers or status code when doing SSR
*/
responseMeta({ clientErrors }) {
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
// for app caching with SSR see https://trpc.io/docs/caching
return {};
},
})(MyApp);

View File

@@ -0,0 +1,35 @@
/**
* This file contains tRPC's HTTP response handler
*/
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from 'server/routers/_app';
import { createContext } from 'server/context';
export default trpcNext.createNextApiHandler({
router: appRouter,
/**
* @link https://trpc.io/docs/context
*/
createContext,
/**
* @link https://trpc.io/docs/error-handling
*/
onError({ error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
console.error('Something went wrong', error);
}
},
/**
* Enable query batching
*/
batching: {
enabled: true,
},
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
// responseMeta() {
// // ...
// },
});

View File

@@ -0,0 +1,120 @@
import Head from 'next/head';
import Link from 'next/link';
import { ReactQueryDevtools } from 'react-query/devtools';
import { trpc } from '../utils/trpc';
export default function IndexPage() {
const postsQuery = trpc.useQuery(['post.all']);
const addPost = trpc.useMutation('post.add');
const utils = trpc.useContext();
// prefetch all posts for instant navigation
// useEffect(() => {
// postsQuery.data?.forEach((post) => {
// utils.prefetchQuery(['post.byId', post.id]);
// });
// }, [postsQuery.data, utils]);
return (
<>
<Head>
<title>Prisma Starter</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Welcome to your tRPC starter!</h1>
<p>
Check <a href="https://trpc.io/docs">the docs</a> whenever you get
stuck, or ping <a href="https://twitter.com/alexdotjs">@alexdotjs</a> on
Twitter.
</p>
<h2>
Posts
{postsQuery.status === 'loading' && '(loading)'}
</h2>
{postsQuery.data?.map((item) => (
<article key={item.id}>
<h3>{item.title}</h3>
<Link href={`/post/${item.id}`}>
<a>View more</a>
</Link>
</article>
))}
<hr />
<form
onSubmit={async (e) => {
e.preventDefault();
/**
* In a real app you probably don't want to use this manually
* Checkout React Hook Form - it works great with tRPC
* @link https://react-hook-form.com/
*/
const $text: HTMLInputElement = (e as any).target.elements.text;
const $title: HTMLInputElement = (e as any).target.elements.title;
const input = {
title: $title.value,
text: $text.value,
};
try {
await addPost.mutateAsync(input);
utils.invalidateQuery(['post.all']);
$title.value = '';
$text.value = '';
} catch {}
}}
>
<label htmlFor="title">Title:</label>
<br />
<input
id="title"
name="title"
type="text"
disabled={addPost.isLoading}
/>
<br />
<label htmlFor="text">Text:</label>
<br />
<textarea id="text" name="text" disabled={addPost.isLoading} />
<br />
<input type="submit" disabled={addPost.isLoading} />
{addPost.error && (
<p style={{ color: 'red' }}>{addPost.error.message}</p>
)}
</form>
{process.env.NODE_ENV !== 'production' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</>
);
}
/**
* If you want to statically render this page
* - Export `appRouter` & `createContext` from [trpc].ts
* - Make the `opts` object optional on `createContext()`
*
* @link https://trpc.io/docs/ssg
*/
// export const getStaticProps = async (
// context: GetStaticPropsContext<{ filter: string }>,
// ) => {
// const ssg = createSSGHelpers({
// router: appRouter,
// ctx: await createContext(),
// });
//
// await ssg.fetchQuery('post.all');
//
// return {
// props: {
// trpcState: ssg.dehydrate(),
// filter: context.params?.filter ?? 'all',
// },
// revalidate: 1,
// };
// };

View File

@@ -0,0 +1,29 @@
import { useRouter } from 'next/dist/client/router';
import { trpc } from 'utils/trpc';
import NextError from 'next/error';
export default function PostViewPage() {
const id = useRouter().query.id as string;
const postQuery = trpc.useQuery(['post.byId', id]);
if (postQuery.error) {
return (
<NextError
title={postQuery.error.message}
statusCode={postQuery.error.data?.httpStatus ?? 500}
/>
);
}
if (postQuery.status !== 'success') {
return <>Loading...</>;
}
const { data } = postQuery;
return (
<>
<h1>{data.title}</h1>
<p>{data.text}</p>
<h2>Raw data:</h2>
<pre>{JSON.stringify(data, null, 4)}</pre>
</>
);
}

View File

@@ -0,0 +1,27 @@
import { PrismaClient } from '@prisma/client';
import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
const prisma = new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
// for API-response caching see https://trpc.io/docs/caching
return {
req,
res,
prisma,
};
};
export type Context = trpc.inferAsyncReturnType<typeof createContext>;

View File

@@ -0,0 +1,9 @@
import * as trpc from '@trpc/server';
import { Context } from './context';
/**
* Helper function to create a router with context
*/
export function createRouter() {
return trpc.router<Context>();
}

View File

@@ -0,0 +1,27 @@
/**
* This file contains the root router of your tRPC-backend
*/
import superjson from 'superjson';
import { createRouter } from '../createRouter';
import { postRouter } from './post';
/**
* Create your application's root router
* If you want to use SSG, you need export this
* @link https://trpc.io/docs/ssg
* @link https://trpc.io/docs/router
*/
export const appRouter = createRouter()
/**
* Add data transformers
* @link https://trpc.io/docs/data-transformers
*/
.transformer(superjson)
/**
* Optionally do custom error (type safe!) formatting
* @link https://trpc.io/docs/error-formatting
*/
// .formatError(({ shape, error }) => { })
.merge('post.', postRouter);
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,86 @@
/**
*
* This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx`
*/
import { createRouter } from 'server/createRouter';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
export const postRouter = createRouter()
// create
.mutation('add', {
input: z.object({
id: z.string().uuid().optional(),
title: z.string().min(1).max(32),
text: z.string().min(1),
}),
async resolve({ ctx, input }) {
const post = await ctx.prisma.post.create({
data: input,
});
return post;
},
})
// read
.query('all', {
async resolve({ ctx }) {
/**
* For pagination you can have a look at this docs site
* @link https://trpc.io/docs/useInfiniteQuery
*/
return ctx.prisma.post.findMany({
select: {
id: true,
title: true,
},
});
},
})
.query('byId', {
input: z.string(),
async resolve({ ctx, input }) {
const post = await ctx.prisma.post.findUnique({
where: { id: input },
select: {
id: true,
title: true,
text: true,
},
});
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `No post with id '${input}'`,
});
}
return post;
},
})
// update
.mutation('edit', {
input: z.object({
id: z.string().uuid(),
data: z.object({
title: z.string().min(1).max(32).optional(),
text: z.string().min(1).optional(),
}),
}),
async resolve({ ctx, input }) {
const { id, data } = input;
const post = await ctx.prisma.post.update({
where: { id },
data,
});
return post;
},
})
// delete
.mutation('delete', {
input: z.string().uuid(),
async resolve({ input: id, ctx }) {
await ctx.prisma.post.delete({ where: { id } });
return id;
},
});

View File

@@ -0,0 +1,20 @@
import { createReactQueryHooks } from '@trpc/react';
import type { inferProcedureOutput } from '@trpc/server';
// Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import type { AppRouter } from 'server/routers/_app';
/**
* A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
* @link https://trpc.io/docs/react#3-create-trpc-hooks
*/
export const trpc = createReactQueryHooks<AppRouter>();
// export const transformer = superjson;
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = inferQueryOutput<'hello'>
*/
export type inferQueryOutput<
TRouteKey extends keyof AppRouter['_def']['queries'],
> = inferProcedureOutput<AppRouter['_def']['queries'][TRouteKey]>;

View File

@@ -0,0 +1,14 @@
jest.setTimeout(35e3);
test('go to /', async () => {
await page.goto('http://localhost:3000');
await page.waitForSelector(`text=Starter`);
});
test('test 404', async () => {
const res = await page.goto('http://localhost:3000/post/not-found');
expect(res?.status()).toBe(404);
});
export {};

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"types": ["@types/jest", "jest-playwright-preset", "expect-playwright"],
"baseUrl": "./src"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}