Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ NODE_ENV=development
# Public gitsheets data repo
# ---------------------------------------------------------------------------

# Absolute path (or relative from repo root) to the codeforphilly-data working tree.
# Clone https://github.com/CodeForPhilly/codeforphilly-data-snapshot as a sibling:
# git clone https://github.com/CodeForPhilly/codeforphilly-data-snapshot ../codeforphilly-data
# Path to the codeforphilly-data working tree. Relative paths are resolved
# from the API workspace dir (`apps/api/`) — prefer absolute paths in dev.
# Bootstrap a fresh dev data repo with the sheet configs the API needs:
# npm run -w apps/api script:setup-dev-data
# (Uses this CFP_DATA_REPO_PATH; creates the directory + git repo + .gitsheets/<sheet>.toml.)
# In production, point at a clone of https://github.com/CodeForPhilly/codeforphilly-data.
CFP_DATA_REPO_PATH=../codeforphilly-data

# Git remote URL to push public data commits to. Optional in dev; required in prod.
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ build/
# it ships in the code repo so contributors can load a fake-data baseline.
private-storage/

# Dev data repo created by `npm run -w apps/api script:setup-dev-data`.
# Resolved relative to apps/api/ per .env.example's CFP_DATA_REPO_PATH default.
apps/codeforphilly-data/
codeforphilly-data/

# Agent worktrees (claude-code background agents create these here).
.claude/worktrees/

# Editor / OS
.DS_Store
.vscode/*
Expand Down
5 changes: 3 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"dev": "tsx watch --env-file=../../.env src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"type-check": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"script:scrub-data": "tsx scripts/scrub-data.ts"
"script:scrub-data": "tsx scripts/scrub-data.ts",
"script:setup-dev-data": "tsx scripts/setup-dev-data.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1048.0",
Expand Down
105 changes: 105 additions & 0 deletions apps/api/scripts/setup-dev-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Seed a local development data repo with the minimal sheet configs the API
// needs to boot. Run once after `cp .env.example .env`:
//
// npm run -w apps/api script:setup-dev-data [-- <path>]
//
// `<path>` defaults to the value of `CFP_DATA_REPO_PATH` in the env, or
// `../codeforphilly-data` if unset.
//
// What this does:
// 1. Creates the directory if missing
// 2. `git init -b main` if not already a repo
// 3. Writes one `.gitsheets/<sheet>.toml` per declared sheet (minimal
// `[gitsheet]` block — root + a placeholder path template)
// 4. Commits as `initial: dev data repo configs`
//
// The path templates here are dev-stub defaults, not authoritative. The real
// templates are authored alongside the production data repo. For an empty
// dev repo with no records, the path template doesn't matter at boot — only
// the existence of `.gitsheets/<sheet>.toml` does.

import { execFile } from 'node:child_process';
import { existsSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve, join } from 'node:path';
import { promisify } from 'node:util';

const exec = promisify(execFile);

const SHEET_CONFIGS: ReadonlyArray<{ name: string; root: string; path: string }> = [
{ name: 'people', root: 'people', path: '${{ slug }}' },
{ name: 'projects', root: 'projects', path: '${{ slug }}' },
{ name: 'project-memberships', root: 'project-memberships', path: '${{ projectSlug }}/${{ personSlug }}' },
{ name: 'project-updates', root: 'project-updates', path: '${{ projectSlug }}/${{ number }}' },
{ name: 'project-buzz', root: 'project-buzz', path: '${{ projectSlug }}/${{ slug }}' },
{ name: 'help-wanted-roles', root: 'help-wanted-roles', path: '${{ projectSlug }}/${{ slug }}' },
{ name: 'help-wanted-interest', root: 'help-wanted-interest', path: '${{ roleId }}/${{ personId }}' },
{ name: 'tags', root: 'tags', path: '${{ namespace }}/${{ slug }}' },
{ name: 'tag-assignments', root: 'tag-assignments', path: '${{ taggableType }}/${{ taggableId }}/${{ tagId }}' },
{ name: 'slug-history', root: 'slug-history', path: '${{ entityType }}/${{ slug }}' },
{ name: 'revocations', root: 'revocations', path: '${{ jti }}' },
];

function configToml(root: string, path: string): string {
return `[gitsheet]\nroot = '${root}'\npath = '${path}'\n`;
}

async function main(): Promise<void> {
const argPath = process.argv[2];
const envPath = process.env['CFP_DATA_REPO_PATH'];
const targetPath = resolve(argPath ?? envPath ?? '../codeforphilly-data');

console.log(`[setup-dev-data] target: ${targetPath}`);

if (!existsSync(targetPath)) {
await mkdir(targetPath, { recursive: true });
console.log(`[setup-dev-data] created directory`);
}

const git = async (...args: string[]): Promise<string> => {
const { stdout } = await exec('git', args, { cwd: targetPath });
return stdout;
};

if (!existsSync(join(targetPath, '.git'))) {
await git('init', '-b', 'main');
await git('config', 'user.email', 'dev@local');
await git('config', 'user.name', 'Dev');
await git('commit', '--allow-empty', '-m', 'initial');
console.log(`[setup-dev-data] initialized git repo`);
}

const gitsheetsDir = join(targetPath, '.gitsheets');
await mkdir(gitsheetsDir, { recursive: true });

let wroteAny = false;
for (const { name, root, path } of SHEET_CONFIGS) {
const configPath = join(gitsheetsDir, `${name}.toml`);
if (existsSync(configPath)) {
console.log(`[setup-dev-data] skip ${name}.toml (already exists)`);
continue;
}
await writeFile(configPath, configToml(root, path), 'utf8');
wroteAny = true;
console.log(`[setup-dev-data] wrote ${name}.toml`);
}

if (!wroteAny) {
console.log(`[setup-dev-data] all sheet configs already present — nothing to commit`);
return;
}

await git('add', '.gitsheets');
const { stdout: porcelain } = await exec('git', ['status', '--porcelain'], { cwd: targetPath });
if (porcelain.trim() === '') {
console.log(`[setup-dev-data] nothing to commit (configs were unchanged)`);
return;
}
await git('commit', '-m', 'initial: dev data repo configs');
console.log(`[setup-dev-data] committed sheet configs`);
}

main().catch((err: unknown) => {
console.error(`[setup-dev-data] failed:`, err);
process.exit(1);
});