Skip to content

Commit 73f9962

Browse files
author
Mikael Vesavuori
committed
Add session 5
1 parent 9a28fc7 commit 73f9962

34 files changed

+1476
-0
lines changed

05-storage-and-databases/README.md

Lines changed: 519 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": ["eslint:recommended", "prettier"],
3+
"parserOptions": {
4+
"ecmaVersion": 11,
5+
"sourceType": "module",
6+
"parser": "babel-eslint"
7+
},
8+
"env": {
9+
"node": true,
10+
"es6": true
11+
}
12+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
### Node ###
2+
.serverless
3+
4+
# Reports
5+
*.report.html
6+
*.report.csv
7+
*.report.json
8+
9+
# Dependency directories
10+
/**/node_modules
11+
12+
# dotenv environment variables file(s)
13+
.env
14+
.env.*
15+
16+
# Build generated
17+
dist/
18+
public/
19+
build/
20+
.next
21+
22+
# Compressed archives
23+
*.7zip
24+
*.rar
25+
*.zip
26+
27+
### VisualStudioCode ###
28+
.vscode/*
29+
!.vscode/settings.json
30+
!.vscode/tasks.json
31+
!.vscode/launch.json
32+
!.vscode/extensions.json
33+
34+
### Vim ###
35+
*.sw[a-p]
36+
37+
# Windows thumbnail cache files
38+
Thumbs.db
39+
ehthumbs.db
40+
ehthumbs_vista.db
41+
42+
# Folder config file
43+
Desktop.ini
44+
45+
# Recycle Bin used on file shares
46+
$RECYCLE.BIN/
47+
48+
### macOS ###
49+
**/.DS_Store
50+
*.DS_Store
51+
.AppleDouble
52+
.LSOverride
53+
54+
# Icon must end with two \r
55+
Icon
56+
57+
# Thumbnails
58+
._*
59+
60+
# Files that might appear in the root of a volume
61+
.DocumentRevisions-V100
62+
.fseventsd
63+
.Spotlight-V100
64+
.TemporaryItems
65+
.Trashes
66+
.VolumeIcon.icns
67+
.com.apple.timemachine.donotpresent
68+
69+
# Directories potentially created on remote AFP share
70+
.AppleDB
71+
.AppleDesktop
72+
Network Trash Folder
73+
Temporary Items
74+
.apdisk
75+
76+
### Node ###
77+
# Logs
78+
logs
79+
*.log
80+
npm-debug.log*
81+
yarn-debug.log*
82+
yarn-error.log*
83+
84+
# Runtime data
85+
pids
86+
*.pid
87+
*.seed
88+
*.pid.lock
89+
90+
# Directory for instrumented libs generated by jscoverage/JSCover
91+
lib-cov
92+
93+
# Coverage directory used by tools like istanbul
94+
coverage
95+
96+
# Optional npm cache directory
97+
.npm
98+
99+
# Optional eslint cache
100+
.eslintcache
101+
102+
# Optional REPL history
103+
.node_repl_history
104+
105+
# Output of 'npm pack'
106+
*.tgz
107+
108+
# Yarn Integrity file
109+
.yarn-integrity
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"useTabs": true,
3+
"printWidth": 100,
4+
"tabWidth": 2,
5+
"singleQuote": true,
6+
"trailingComma": "none"
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
const { ApolloServer } = require('apollo-server-cloud-functions');
4+
const { typeDefs, resolvers } = require('./schema');
5+
6+
const apolloServer = new ApolloServer({
7+
typeDefs,
8+
resolvers,
9+
introspection: true,
10+
resolverValidationOptions: {
11+
requireResolversForResolveType: false
12+
}
13+
});
14+
15+
const server = apolloServer.createHandler({
16+
cors: {
17+
origin: '*' // This is just for playing with; please set this to your actual allowed domain
18+
}
19+
});
20+
21+
module.exports = server;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Dependencies
2+
const { gql } = require('apollo-server-cloud-functions');
3+
4+
// Helper functions
5+
const { invokeFunction } = require('../helpers/invokeFunction');
6+
7+
// New functions that use the database
8+
const { createArtwork } = require('../functions/api/createArtwork');
9+
const { readArtworks } = require('../functions/api/readArtworks');
10+
const { readArtworksByTitle } = require('../functions/api/readArtworksByTitle');
11+
const { readArtworksByFilter } = require('../functions/api/readArtworksByFilter');
12+
const { updateArtwork } = require('../functions/api/updateArtwork');
13+
const { deleteArtwork } = require('../functions/api/deleteArtwork');
14+
15+
// Configuration
16+
const { REGION, PROJECT_ID, TOPIC_NAME } = require('../configuration');
17+
18+
const typeDefs = gql`
19+
# The basic shape of an artwork
20+
type Artwork {
21+
artist: String
22+
imageUrl: String
23+
originalImageUrl: String
24+
title: String
25+
year: Int
26+
uuid: String
27+
labels: [String]
28+
createdByUser: String
29+
}
30+
31+
# The shape of artwork input data
32+
input ArtworkInput {
33+
artist: String!
34+
imageUrl: String!
35+
title: String
36+
year: Int
37+
createdByUser: String
38+
}
39+
40+
# The shape of artwork (update) input data
41+
input UpdateArtworkInput {
42+
artist: String
43+
imageUrl: String
44+
title: String
45+
year: Int
46+
uuid: String!
47+
}
48+
49+
# The basic shape of an image
50+
type ImageLabels {
51+
labels: [String]
52+
}
53+
54+
type ArtworkDbUpdateEvent {
55+
artwork: Artwork
56+
}
57+
58+
type ArtworkDbDeleteEvent {
59+
uuid: String
60+
}
61+
62+
# Any queries that can be made
63+
type Query {
64+
getArtworks: [Artwork]
65+
getArtworkByTitle(title: String!): [Artwork]
66+
getArtworkByFilter(
67+
artist: String
68+
imageUrl: String
69+
originalImageUrl: String
70+
title: String
71+
year: Int
72+
uuid: String
73+
labels: [String]
74+
createdByUser: String
75+
): [Artwork]
76+
}
77+
78+
# Any mutations that can be made
79+
type Mutation {
80+
getLabels(imageUrl: String!): ImageLabels
81+
createArtwork(artwork: ArtworkInput!): Artwork
82+
updateArtwork(artwork: UpdateArtworkInput!): ArtworkDbUpdateEvent
83+
deleteArtwork(uuid: String!): ArtworkDbDeleteEvent
84+
}
85+
`;
86+
87+
const resolvers = {
88+
Query: {
89+
// Get all artworks from Firestore
90+
getArtworks: async () => await readArtworks(),
91+
92+
// Get individual artwork by title in the typical REST+SQL style; note that the second parameter is 'arguments'
93+
getArtworkByTitle: async (_, { title }) => await readArtworksByTitle(title),
94+
95+
// Get artwork(s) in the GQL+NoSQL style with filtering just before sending back to client
96+
getArtworkByFilter: async (_, args) => await readArtworksByFilter(args)
97+
},
98+
Mutation: {
99+
// External function
100+
getLabels: async (_, { imageUrl }) => {
101+
return new Promise(async (resolve, reject) => {
102+
const ENDPOINT = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/getLabels`; // Make sure that this exactly matches your own function name from the image labeling service that you should have deployed (https://github.com/mikaelvesavuori/gcp-ml-image-labeling-service)
103+
const res = await invokeFunction(ENDPOINT, imageUrl);
104+
resolve(res);
105+
}).then(labels => {
106+
console.log('Labels:', labels);
107+
return { labels };
108+
});
109+
},
110+
// Local bundled CRUD functions
111+
createArtwork: async (_, { artwork }) => await createArtwork(artwork),
112+
updateArtwork: async (_, { artwork }) => await updateArtwork(artwork),
113+
deleteArtwork: async (_, { uuid }) => await deleteArtwork(uuid)
114+
}
115+
};
116+
117+
module.exports = { typeDefs, resolvers };
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Fill this out for session 4
2+
exports.TOPIC_NAME = '';
3+
4+
// Fill these out for session 4 and 5
5+
exports.REGION = 'europe-west1'; // EDIT THIS ONLY IF NOT FOLLOWING EXACTLY STEP-BY-STEP
6+
exports.PROJECT_ID = '';
7+
8+
// Fill these out for session 5
9+
exports.BUCKET_NAME = '';
10+
exports.COLLECTION_NAME = 'artworks'; // EDIT THIS ONLY IF NOT FOLLOWING EXACTLY STEP-BY-STEP
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict';
2+
3+
const Firestore = require('@google-cloud/firestore');
4+
const firestore = new Firestore();
5+
const uuidv4 = require('uuid/v4');
6+
7+
const { uploadImage } = require('../image/uploadImage');
8+
const { invokeFunction } = require('../../helpers/invokeFunction');
9+
const { REGION, PROJECT_ID, BUCKET_NAME, COLLECTION_NAME } = require('../../configuration');
10+
11+
/**
12+
* Creates artwork in Firestore
13+
*
14+
* @async
15+
* @function
16+
* @param {string} artist - The artist's name
17+
* @param {string} imageUrl - URL to the image
18+
* @param {string} title - Title of the artwork
19+
* @param {number} year - Year of the artwork's creation
20+
* @param {string} createdByUser - The name of the user who created this artwork listing
21+
* @returns {object} - Returns artwork object
22+
* @throws {error} - Throws error if it fails to create in Firestore
23+
*/
24+
exports.createArtwork = async ({ artist, imageUrl, title, year, createdByUser }) => {
25+
if (artist && imageUrl && title && year && createdByUser) {
26+
const UUID = uuidv4();
27+
28+
const FILE_FORMAT_SPLIT_POINT = imageUrl.lastIndexOf('.');
29+
const FILE_FORMAT = imageUrl.slice(FILE_FORMAT_SPLIT_POINT, imageUrl.length);
30+
const IMAGE_NAME = `${UUID}${FILE_FORMAT}`;
31+
32+
const DOCUMENT = {
33+
artist,
34+
originalImageUrl: imageUrl,
35+
imageUrl: `https://storage.cloud.google.com/${BUCKET_NAME}/${IMAGE_NAME}`,
36+
title,
37+
year,
38+
uuid: UUID,
39+
labels: [],
40+
createdByUser
41+
};
42+
43+
const upload = await new Promise(async (resolve, reject) => {
44+
try {
45+
await uploadImage(imageUrl, IMAGE_NAME, BUCKET_NAME);
46+
resolve();
47+
} catch (error) {
48+
reject(error);
49+
}
50+
});
51+
52+
const getLabels = await new Promise(async (resolve, reject) => {
53+
try {
54+
// Make sure that this exactly matches your own function name from the image labeling service that you should have deployed (https://github.com/mikaelvesavuori/gcp-ml-image-labeling-service)
55+
const ENDPOINT = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/getLabels`;
56+
const labels = await invokeFunction(ENDPOINT, { imageUrl: imageUrl })
57+
.then(labels => {
58+
DOCUMENT.labels = labels;
59+
console.log('createArtwork ||| Document that will be set in database:', DOCUMENT);
60+
})
61+
.then(res => resolve(res));
62+
} catch (error) {
63+
console.error('createArtwork ||| Failed to get labels from image!', error);
64+
reject(error);
65+
}
66+
});
67+
68+
const addEntry = await new Promise(async (resolve, reject) => {
69+
try {
70+
await firestore
71+
.doc(`${COLLECTION_NAME}/${UUID}`)
72+
.set(DOCUMENT)
73+
.then(() => {
74+
resolve();
75+
})
76+
.catch(error => {
77+
console.error('createArtwork ||| Error creating entry in Firestore!');
78+
reject(error);
79+
});
80+
} catch (error) {
81+
console.error('createArtwork ||| Failed adding entry to Firestore!', error);
82+
reject(error);
83+
}
84+
});
85+
86+
return await Promise.all([upload, getLabels, addEntry])
87+
.then(() => {
88+
return DOCUMENT;
89+
})
90+
.catch(error => {
91+
return error;
92+
});
93+
} else {
94+
const ERROR_MESSAGE =
95+
'Missing one or more required fields: "artist", "imageUrl", "title", "year", "createdByUser"!';
96+
throw new Error(ERROR_MESSAGE);
97+
}
98+
};

0 commit comments

Comments
 (0)