Skip to content

Commit

Permalink
feat: Add tRPC application server and Firestore database (#2063)
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya authored Jan 8, 2024
1 parent b34bced commit 1e79ac9
Show file tree
Hide file tree
Showing 46 changed files with 2,877 additions and 177 deletions.
7 changes: 7 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ APP_NAME=Acme Co.
APP_HOSTNAME=localhost
APP_ORIGIN=http://localhost:5173
API_ORIGIN=https://api-mcfytwakla-uc.a.run.app
APP_STORAGE_BUCKET=example.com

# Google Cloud
# https://console.cloud.google.com/
GOOGLE_CLOUD_PROJECT=kriasoft
GOOGLE_CLOUD_REGION=us-central1
GOOGLE_CLOUD_DATABASE="(default)"
GOOGLE_CLOUD_CREDENTIALS={"type":"service_account","project_id":"example","private_key_id":"xxx","private_key":"-----BEGIN PRIVATE KEY-----\nxxxxx\n-----END PRIVATE KEY-----\n","client_email":"[email protected]","client_id":"xxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/application%40example.iam.gserviceaccount.com"}

# Firebase
Expand All @@ -24,6 +26,11 @@ FIREBASE_APP_ID=1:736557952746:web:b5ee23841e24c0b883b193
FIREBASE_API_KEY=AIzaSyAZDmdeRWvlYgZpwm6LBCkYJM6ySIMF2Hw
FIREBASE_AUTH_DOMAIN=kriasoft.web.app

# OpenAI
# https://platform.openai.com/
OPENAI_ORGANIZATION=xxxxx
OPENAI_API_KEY=xxxxx

# Cloudflare
# https://dash.cloudflare.com/
# https://developers.cloudflare.com/api/tokens/create
Expand Down
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,28 @@
"firestore",
"globby",
"hono",
"identitytoolkit",
"jamstack",
"kriasoft",
"localforage",
"miniflare",
"nodenext",
"notistack",
"oidc",
"openai",
"pathinfo",
"pino",
"pnpify",
"reactstarter",
"refetch",
"refetchable",
"relyingparty",
"sendgrid",
"signup",
"sourcemap",
"spdx",
"swapi",
"trpc",
"tslib",
"typechecking",
"vite",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ Be sure to join our [Discord channel](https://discord.com/invite/2nKEnKq) for as
`├──`[`.github`](.github) — GitHub configuration including CI/CD workflows<br>
`├──`[`.vscode`](.vscode) — VSCode settings including code snippets, recommended extensions etc.<br>
`├──`[`app`](./app) — Web application front-end built with [React](https://react.dev/) and [Joy UI](https://mui.com/joy-ui/getting-started/)<br>
`├──`[`db`](./db) — Firestore database schema, seed data, and admin tools<br>
`├──`[`edge`](./edge) — Cloudflare Workers (CDN) edge endpoint<br>
`├──`[`env`](./env) — Application settings, API keys, etc.<br>
`├──`[`scripts`](./scripts) — Automation scripts such as `yarn deploy`<br>
`├──`[`server`](./server) — Node.js application server built with [tRPC](https://trpc.io/)<br>
`├──`[`tsconfig.base.json`](./tsconfig.base.json) — The common/shared TypeScript configuration<br>
`└──`[`tsconfig.json`](./tsconfig.json) — The root TypeScript configuration<br>

Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"devDependencies": {
"@babel/core": "^7.23.7",
"@emotion/babel-plugin": "^11.11.0",
"@types/node": "^20.10.6",
"@types/node": "^20.10.7",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
Expand Down
20 changes: 20 additions & 0 deletions db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Firestore Database

Database schema, security rules, indexes, and seed data for the [Firestore](https://cloud.google.com/firestore) database.

## Directory Structure

- [`/models`](./models/) — Database schema definitions using [Zod](https://zod.dev/).
- [`/seeds`](./seeds/) — Sample / reference data for the database.
- [`/scripts`](./scripts/) — Scripts for managing the database.
- [`/firestore.indexes.json`](./firestore.indexes.json) — Firestore indexes.
- [`/firestore.rules`](./firestore.rules) — Firestore security rules.

## Scripts

- `yarn workspace db seed` - Seed the database with data from [`/seeds`](./seeds/).

## References

- https://zod.dev/
- https://cloud.google.com/firestore
13 changes: 13 additions & 0 deletions db/firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"indexes": [
{
"collectionGroup": "workspace",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "ownerId", "order": "ASCENDING" },
{ "fieldPath": "archived", "order": "DESCENDING" }
]
}
],
"fieldOverrides": []
}
19 changes: 19 additions & 0 deletions db/firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Firestore security rules.
// https://cloud.google.com/firestore/docs/security/get-started

rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {
match /workspace/{id} {
allow read: if request.auth != null && (
resource.data.ownerId = request.auth.uid ||
request.auth.token.admin == true
);
}

match /{document=**} {
allow read, write: if false;
}
}
}
6 changes: 6 additions & 0 deletions db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

export * from "./models";
export { testUsers } from "./seeds/01-users";
export { testWorkspaces } from "./seeds/02-workspaces";
4 changes: 4 additions & 0 deletions db/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

export * from "./workspace";
16 changes: 16 additions & 0 deletions db/models/workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

import { Timestamp } from "@google-cloud/firestore";
import { z } from "zod";

export const WorkspaceSchema = z.object({
name: z.string().max(100),
ownerId: z.string().max(50),
created: z.instanceof(Timestamp),
updated: z.instanceof(Timestamp),
archived: z.instanceof(Timestamp).nullable(),
});

export type Workspace = z.output<typeof WorkspaceSchema>;
export type WorkspaceInput = z.input<typeof WorkspaceSchema>;
30 changes: 30 additions & 0 deletions db/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "db",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"default": "./index.ts"
},
"./package.json": "./package.json"
},
"scripts": {
"seed": "vite-node ./scripts/seed.ts",
"test": "vitest"
},
"dependencies": {
"@google-cloud/firestore": "^7.1.0",
"@googleapis/identitytoolkit": "^8.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.7",
"dotenv": "^16.3.1",
"ora": "^8.0.1",
"typescript": "~5.3.3",
"vite": "~5.0.11",
"vite-node": "~1.1.3",
"vitest": "~1.1.3"
}
}
43 changes: 43 additions & 0 deletions db/scripts/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

import { Firestore } from "@google-cloud/firestore";
import { configDotenv } from "dotenv";
import { relative, resolve } from "node:path";
import { oraPromise } from "ora";

const rootDir = resolve(__dirname, "../..");

// Load environment variables from .env files.
configDotenv({ path: resolve(rootDir, ".env.local") });
configDotenv({ path: resolve(rootDir, ".env") });

let db: Firestore | null = null;

// Seed the database with test / sample data.
try {
db = new Firestore({
projectId: process.env.GOOGLE_CLOUD_PROJECT,
databaseId: process.env.GOOGLE_CLOUD_DATABASE,
});

// Import all seed modules from the `/seeds` folder.
const files = import.meta.glob<boolean, string, SeedModule>("../seeds/*.ts");

// Sequentially seed the database with data from each module.
for (const [path, load] of Object.entries(files)) {
const message = `Seeding ${relative("../seeds", path)}`;
const action = (async () => {
const { seed } = await load();
await seed(db);
})();

await oraPromise(action, message);
}
} finally {
await db?.terminate();
}

type SeedModule = {
seed: (db: Firestore) => Promise<void>;
};
87 changes: 87 additions & 0 deletions db/seeds/01-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

import {
AuthPlus,
identitytoolkit,
identitytoolkit_v3,
} from "@googleapis/identitytoolkit";

/**
* Test user accounts generated by https://randomuser.me/.
*/
export const testUsers: identitytoolkit_v3.Schema$UserInfo[] = [
{
localId: "test-erika",
screenName: "erika",
email: "[email protected]",
emailVerified: true,
phoneNumber: "+14788078434",
displayName: "Erika Pearson",
photoUrl: "https://randomuser.me/api/portraits/women/29.jpg",
rawPassword: "paloma",
createdAt: new Date("2024-01-01T12:00:00Z").getTime().toString(),
lastLoginAt: new Date("2024-01-01T12:00:00Z").getTime().toString(),
},
{
localId: "test-ryan",
screenName: "ryan",
email: "[email protected]",
emailVerified: true,
phoneNumber: "+16814758216",
displayName: "Ryan Hunt",
photoUrl: "https://randomuser.me/api/portraits/men/20.jpg",
rawPassword: "baggins",
createdAt: new Date("2024-01-02T12:00:00Z").getTime().toString(),
lastLoginAt: new Date("2024-01-02T12:00:00Z").getTime().toString(),
},
{
localId: "test-marian",
screenName: "marian",
email: "[email protected]",
emailVerified: true,
phoneNumber: "+19243007975",
displayName: "Marian Stone",
photoUrl: "https://randomuser.me/api/portraits/women/2.jpg",
rawPassword: "winter1",
createdAt: new Date("2024-01-03T12:00:00Z").getTime().toString(),
lastLoginAt: new Date("2024-01-03T12:00:00Z").getTime().toString(),
},
{
localId: "test-kurt",
screenName: "kurt",
email: "[email protected]",
emailVerified: true,
phoneNumber: "+19243007975",
displayName: "Kurt Howard",
photoUrl: "https://randomuser.me/api/portraits/men/23.jpg",
rawPassword: "mayday",
createdAt: new Date("2024-01-04T12:00:00Z").getTime().toString(),
lastLoginAt: new Date("2024-01-04T12:00:00Z").getTime().toString(),
},
{
localId: "test-dan",
screenName: "dan",
email: "[email protected]",
emailVerified: true,
phoneNumber: "+12046748092",
displayName: "Dan Day",
photoUrl: "https://randomuser.me/api/portraits/men/65.jpg",
rawPassword: "teresa",
createdAt: new Date("2024-01-05T12:00:00Z").getTime().toString(),
lastLoginAt: new Date("2024-01-05T12:00:00Z").getTime().toString(),
customAttributes: JSON.stringify({ admin: true }),
},
];

/**
* Seeds the Google Identity Platform (Firebase Auth) with test user accounts.
*
* @see https://randomuser.me/
* @see https://cloud.google.com/identity-platform
*/
export async function seed() {
const auth = new AuthPlus();
const { relyingparty } = identitytoolkit({ version: "v3", auth });
await relyingparty.uploadAccount({ requestBody: { users: testUsers } });
}
63 changes: 63 additions & 0 deletions db/seeds/02-workspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* SPDX-FileCopyrightText: 2014-present Kriasoft */
/* SPDX-License-Identifier: MIT */

import { Firestore, Timestamp } from "@google-cloud/firestore";
import { WorkspaceInput } from "../models";
import { testUsers as users } from "./01-users";

/**
* Test workspaces.
*/
export const testWorkspaces: (WorkspaceInput & { id: string })[] = [
{
id: "DwYchGFGpk",
ownerId: users[0].localId!,
name: "Personal workspace",
created: Timestamp.fromDate(new Date(+users[0].createdAt!)),
updated: Timestamp.fromDate(new Date(+users[0].createdAt!)),
archived: null,
},
{
id: "YfYKTcO9q9",
ownerId: users[1].localId!,
name: "Personal workspace",
created: Timestamp.fromDate(new Date(+users[1].createdAt!)),
updated: Timestamp.fromDate(new Date(+users[1].createdAt!)),
archived: null,
},
{
id: "c2OsmUvFMY",
ownerId: users[2].localId!,
name: "Personal workspace",
created: Timestamp.fromDate(new Date(+users[2].createdAt!)),
updated: Timestamp.fromDate(new Date(+users[2].createdAt!)),
archived: null,
},
{
id: "uTqcGw4qn7",
ownerId: users[3].localId!,
name: "Personal workspace",
created: Timestamp.fromDate(new Date(+users[3].createdAt!)),
updated: Timestamp.fromDate(new Date(+users[3].createdAt!)),
archived: null,
},
{
id: "vBHHgg5ydn",
ownerId: users[4].localId!,
name: "Personal workspace",
created: Timestamp.fromDate(new Date(+users[4].createdAt!)),
updated: Timestamp.fromDate(new Date(+users[4].createdAt!)),
archived: null,
},
];

export async function seed(db: Firestore) {
const batch = db.batch();

for (const { id, ...workspace } of testWorkspaces) {
const ref = db.doc(`workspace/${id}`);
batch.set(ref, workspace, { merge: true });
}

await batch.commit();
}
Loading

0 comments on commit 1e79ac9

Please sign in to comment.