diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..26b8810 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +THINGIVERSE_TOKEN= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 22f55ad..ea48d65 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0330a03 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Nest Framework", + "args": ["${workspaceFolder}/src/main.ts"], + "runtimeArgs": ["--nolazy", "-r", "ts-node/register", "-r", "tsconfig-paths/register"], + "sourceMaps": true, + "envFile": "${workspaceFolder}/.env", + "cwd": "${workspaceRoot}", + "console": "integratedTerminal", + } + ] +} diff --git a/package.json b/package.json index cf2d241..1acbf09 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,14 @@ }, "dependencies": { "@nestjs/apollo": "^10.0.19", + "@nestjs/axios": "^0.1.0", "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", "@nestjs/graphql": "^10.0.21", "@nestjs/platform-express": "^9.0.0", "apollo-server-express": "^3.10.1", + "axios-cache-adapter": "^2.7.3", "graphql": "^16.5.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/src/app.module.ts b/src/app.module.ts index afce27b..44f967d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,22 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { join } from 'path'; import { ThingResolver } from './thing/thing.resolver'; +import { httpCacheAdapter } from './utils/httpCacheAdapter'; @Module({ imports: [ + ConfigModule.forRoot(), + HttpModule.register({ + baseURL: 'https://api.thingiverse.com/', + adapter: httpCacheAdapter, + headers: { + Authorization: `Bearer ${process.env.THINGIVERSE_TOKEN}`, + }, + }), GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), diff --git a/src/schema.gql b/src/schema.gql index 7d4f151..57012e6 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -4,8 +4,23 @@ type Thing { id: Int! + name: String! + thumbnail: String! } type Query { - things: [Thing!]! + things(page: Int, perPage: Int, sort: ThingsArgsSort = POPULAR, postedAfter: ThingsArgsPostedAfter): [Thing!]! + thing(id: Float!): Thing! +} + +enum ThingsArgsSort { + POPULAR + NEWEST + MAKES +} + +enum ThingsArgsPostedAfter { + Last7Days + Last30Days + ThisYear } \ No newline at end of file diff --git a/src/thing/dto/things.args.ts b/src/thing/dto/things.args.ts new file mode 100644 index 0000000..addec4d --- /dev/null +++ b/src/thing/dto/things.args.ts @@ -0,0 +1,36 @@ +import { Field, ArgsType, Int, registerEnumType } from '@nestjs/graphql'; + +export enum ThingsArgsSort { + POPULAR = 'popular', + NEWEST = 'newest', + MAKES = 'makes', +} + +registerEnumType(ThingsArgsSort, { + name: 'ThingsArgsSort', +}); + +export enum ThingsArgsPostedAfter { + Last7Days = 'now-7d', + Last30Days = 'now-30d', + ThisYear = 'now-365d', +} + +registerEnumType(ThingsArgsPostedAfter, { + name: 'ThingsArgsPostedAfter', +}); + +@ArgsType() +export class ThingsArgs { + @Field(() => Int, { nullable: true }) + page: number; + + @Field(() => Int, { nullable: true }) + perPage: number; + + @Field(() => ThingsArgsSort, { nullable: true, defaultValue: 'popular' }) + sort: ThingsArgsSort; + + @Field(() => ThingsArgsPostedAfter, { nullable: true }) + postedAfter?: ThingsArgsPostedAfter; +} diff --git a/src/thing/thing.model.ts b/src/thing/thing.model.ts index a4aee73..54b314d 100644 --- a/src/thing/thing.model.ts +++ b/src/thing/thing.model.ts @@ -1,7 +1,13 @@ -import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType, } from '@nestjs/graphql'; @ObjectType() export class Thing { - @Field(type => Int) + @Field(() => Int) id: number; -} \ No newline at end of file + + @Field(() => String) + name: string; + + @Field(() => String) + thumbnail: string; +} diff --git a/src/thing/thing.resolver.ts b/src/thing/thing.resolver.ts index a58bafd..7faef3f 100644 --- a/src/thing/thing.resolver.ts +++ b/src/thing/thing.resolver.ts @@ -1,19 +1,40 @@ -import { Query, Resolver } from '@nestjs/graphql'; +import { HttpService } from '@nestjs/axios'; +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { firstValueFrom } from 'rxjs'; +import { ThingsArgs } from './dto/things.args'; import { Thing } from './thing.model'; @Resolver(() => Thing) export class ThingResolver { - constructor() {} + constructor(private readonly httpService: HttpService) {} @Query(() => [Thing]) - async things(): Promise { - return [ - { - id: 1, - }, - { - id: 2, - }, - ]; + async things(@Args() args: ThingsArgs): Promise { + const params = new URLSearchParams({ + page: args.page.toString(), + per_page: args.perPage.toString(), + sort: args.sort, + posted_after: args.postedAfter, + type: 'trings', + }); + + const queryStirng = `/search?${params.toString()}`; + + const { data } = await firstValueFrom( + this.httpService.get<{ + hits: Thing[]; + total: number; + }>(queryStirng), + ); + + return data.hits; + } + + @Query(() => Thing) + async thing(@Args('id') id: number): Promise { + const { data } = await firstValueFrom( + this.httpService.get(`/things/${id}`), + ); + return data; } } diff --git a/src/utils/httpCacheAdapter.ts b/src/utils/httpCacheAdapter.ts new file mode 100644 index 0000000..ad9839b --- /dev/null +++ b/src/utils/httpCacheAdapter.ts @@ -0,0 +1,5 @@ +import { setupCache } from 'axios-cache-adapter' + +export const httpCacheAdapter = setupCache({ + maxAge: 60 * 60 * 1000, // 1 hour +}).adapter \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 740956b..8f6e8c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -817,6 +817,13 @@ lodash.omit "4.5.0" tslib "2.4.0" +"@nestjs/axios@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-0.1.0.tgz#6cf93df11ef93b598b3c7411adb980eedd13b3e3" + integrity sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w== + dependencies: + axios "0.27.2" + "@nestjs/cli@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-9.0.0.tgz#f9ab4c33cfbc9ee54d792fe29e0f582e0bf04b06" @@ -854,6 +861,16 @@ tslib "2.4.0" uuid "8.3.2" +"@nestjs/config@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.2.0.tgz#9f3da35f7c4a58724c0a0817d6f04b66e6703430" + integrity sha512-78Eg6oMbCy3D/YvqeiGBTOWei1Jwi3f2pSIZcZ1QxY67kYsJzTRTkwRT8Iv30DbK0sGKc1mcloDLD5UXgZAZtg== + dependencies: + dotenv "16.0.1" + dotenv-expand "8.0.3" + lodash "4.17.21" + uuid "8.3.2" + "@nestjs/core@^9.0.0": version "9.0.9" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.0.9.tgz#4582d8ea1d7bf99faac4b6b5208150c2addab458" @@ -1735,6 +1752,22 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +axios-cache-adapter@^2.7.3: + version "2.7.3" + resolved "https://registry.yarnpkg.com/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz#0d1eefa0f25b88f42a95c7528d7345bde688181d" + integrity sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ== + dependencies: + cache-control-esm "1.0.0" + md5 "^2.2.1" + +axios@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + babel-jest@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" @@ -1906,6 +1939,11 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cache-control-esm@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cache-control-esm/-/cache-control-esm-1.0.0.tgz#417647ecf1837a5e74155f55d5a4ae32a84e2581" + integrity sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g== + call-bind@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1969,6 +2007,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + chokidar@3.5.3, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -2187,6 +2230,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + cssfilter@0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" @@ -2280,6 +2328,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv-expand@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" + integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== + +dotenv@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" + integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2710,6 +2768,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + fork-ts-checker-webpack-plugin@7.2.11: version "7.2.11" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" @@ -3074,6 +3137,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" @@ -3751,6 +3819,15 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"