Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .github/workflows/linter.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
- name: Install Dependencies
run: cd template && pnpm install

- name: Build API Types
if: inputs.component == 'template/web'
run: cd template && pnpm --filter api run build:types

- name: Generate Content Collections
if: inputs.component == 'template/web'
run: cd template && pnpm --filter web run content-collections:build
Expand Down
2 changes: 1 addition & 1 deletion docs/web/calling-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const { mutate: update } = useApiMutation(apiClient.projects.update, {
import queryClient from 'query-client';

queryClient.invalidateQueries({ queryKey: [apiClient.projects.list.path] });
queryClient.setQueryData([apiClient.account.get.path], updatedData);
queryClient.setQueryData([apiClient.users.getCurrent.path], updatedData);
```

Query keys = `[endpoint.path, ...params]`. The first element is always the endpoint path string.
Expand Down
3,325 changes: 3,325 additions & 0 deletions packages/node-mongo/pnpm-lock.yaml

Large diffs are not rendered by default.

34 changes: 24 additions & 10 deletions packages/node-mongo/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ class Service<T extends IDocument> {
return query;
};

private omitSecureFields = <U>(doc: U): U => {
if (!this.options.secureFields?.length) return doc;
return _.omit(doc as object, this.options.secureFields) as U;
};

protected validateCreateOperation = async <U extends T = T>(
object: Partial<U>,
createConfig: CreateConfig,
Expand Down Expand Up @@ -245,11 +250,14 @@ class Service<T extends IDocument> {

if (readConfig.populate) {
const docs = await this.populateAggregate<U, PopulateTypes>(collection, filter, readConfig, findOptions);
const doc = docs[0] || null;

return docs[0] || null;
return doc && !readConfig.isIncludeSecureFields ? this.omitSecureFields(doc) : doc;
}

return collection.findOne<U>(filter, findOptions);
const doc = await collection.findOne<U>(filter, findOptions);

return doc && !readConfig.isIncludeSecureFields ? this.omitSecureFields(doc) : doc;
}

// Method overloading for find
Expand All @@ -276,14 +284,18 @@ class Service<T extends IDocument> {

filter = this.handleReadOperations(filter, readConfig);

const omit = !readConfig.isIncludeSecureFields
? <R>(docs: R[]) => docs.map((d) => this.omitSecureFields(d))
: <R>(docs: R[]) => docs;

if (!hasPaging) {
const results = readConfig.populate
? await this.populateAggregate<U, PopulateTypes>(collection, filter, readConfig, findOptions)
: await collection.find<U>(filter, findOptions).toArray();

return {
pagesCount: 1,
results,
results: omit(results),
count: results.length,
};
}
Expand All @@ -302,7 +314,7 @@ class Service<T extends IDocument> {

return {
pagesCount,
results,
results: omit(results),
count,
};
}
Expand Down Expand Up @@ -376,7 +388,7 @@ class Service<T extends IDocument> {
await collection.insertOne(validEntity as OptionalUnlessRequiredId<U>, insertOneOptions);
}

return validEntity;
return createConfig.isIncludeSecureFields ? validEntity : this.omitSecureFields(validEntity);
};

insertMany = async <U extends T = T>(
Expand Down Expand Up @@ -415,7 +427,7 @@ class Service<T extends IDocument> {
await collection.insertMany(validEntities as OptionalUnlessRequiredId<U>[], bulkWriteOptions);
}

return validEntities;
return createConfig.isIncludeSecureFields ? validEntities : validEntities.map((e) => this.omitSecureFields(e));
};

replaceOne = async (
Expand Down Expand Up @@ -505,7 +517,7 @@ class Service<T extends IDocument> {
logger.warn(`Document hasn't changed when updating ${this._collectionName} collection.`);
}

return newDoc;
return updateConfig.isIncludeSecureFields ? newDoc : this.omitSecureFields(newDoc);
}

if (this.options.addUpdatedOnField) {
Expand Down Expand Up @@ -556,7 +568,7 @@ class Service<T extends IDocument> {
);
}

return newDoc;
return updateConfig.isIncludeSecureFields ? newDoc : this.omitSecureFields(newDoc);
}

updateMany<U extends T = T>(
Expand Down Expand Up @@ -640,7 +652,8 @@ class Service<T extends IDocument> {
logger.warn(`Documents hasn't changed when updating ${this._collectionName} collection.`);
}

return updated.filter(Boolean).map((u) => u?.doc) as U[];
const docs = updated.filter(Boolean).map((u) => u?.doc) as U[];
return updateConfig.isIncludeSecureFields ? docs : docs.map((d) => this.omitSecureFields(d));
}

if (this.options.addUpdatedOnField) {
Expand Down Expand Up @@ -701,7 +714,8 @@ class Service<T extends IDocument> {
await collection.bulkWrite(bulkWriteQuery, updateOptions);
}

return updated.map((u) => u?.doc) as U[];
const docs = updated.map((u) => u?.doc) as U[];
return updateConfig.isIncludeSecureFields ? docs : docs.map((d) => this.omitSecureFields(d));
}

deleteOne = async <U extends T = T>(
Expand Down
4 changes: 4 additions & 0 deletions packages/node-mongo/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type FindResult<T> = {
export type CreateConfig = {
validateSchema?: boolean,
publishEvents?: boolean,
isIncludeSecureFields?: boolean,
};

export type PopulateOptions = {
Expand All @@ -79,6 +80,7 @@ export type PopulateOptions = {
export type ReadConfig = {
skipDeletedOnDocs?: boolean,
populate?: PopulateOptions | PopulateOptions[];
isIncludeSecureFields?: boolean,
};

// Type-safe discriminated unions for populate operations
Expand All @@ -94,6 +96,7 @@ export type UpdateConfig = {
skipDeletedOnDocs?: boolean,
validateSchema?: boolean,
publishEvents?: boolean,
isIncludeSecureFields?: boolean,
};

export type DeleteConfig = {
Expand Down Expand Up @@ -128,6 +131,7 @@ interface ServiceOptions {
collectionOptions?: CollectionOptions;
collectionCreateOptions?: CreateCollectionOptions;
escapeRegExp?: boolean;
secureFields?: string[];
}

export type UpdateFilterFunction<U> = (doc: U) => Partial<U>;
Expand Down
82 changes: 28 additions & 54 deletions template/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md — Ship Monorepo

> pnpm monorepo (Turborepo): `apps/api` (Koa + MongoDB), `apps/web` (Next.js Pages Router), `packages/shared` (auto-generated typed API client), plus `app-constants`, `mailer`, config packages.
> pnpm monorepo (Turborepo): `apps/api` (Hono + oRPC + MongoDB), `apps/web` (Next.js Pages Router), plus `app-constants`, `mailer`, config packages. Types flow from API → web via TypeScript declarations (`tsc --emitDeclarationOnly`), no codegen.

---

Expand All @@ -10,9 +10,8 @@
2. **Read the scoped file** nearest to your task:
- API work → `apps/api/AGENTS.md`
- Web work → `apps/web/AGENTS.md`
- Codegen / shared types → `packages/shared/AGENTS.md`
3. **Read the relevant workflow doc** from the index below.
4. **Scan existing code** — search for a similar resource/page/endpoint before creating new patterns.
4. **Scan existing code** — search for a similar resource/page/procedure before creating new patterns.
5. **Plan, implement, verify** — every task ends with a verification command.

---
Expand All @@ -21,80 +20,55 @@

| Doc | When to read |
|-----|-------------|
| `agent_docs/workflows_dev_build_test.md` | Any task: install, dev, build, typecheck, lint, test |
| `agent_docs/workflows_dev_build_test.md` | Any task: install, dev, build, typecheck, lint |
| `agent_docs/api_resource_and_endpoint_workflow.md` | Adding/modifying API resources or endpoints |
| `agent_docs/web_pages_and_data_access.md` | Adding/modifying web pages or API data consumption |
| `agent_docs/shared_codegen_contract.md` | After any API endpoint/schema change; understanding the type bridge |
| `agent_docs/common_failure_modes.md` | When debugging errors or before submitting changes |
| `apps/api/AGENTS.md` | API-specific invariants (middleware, services, config) |
| `apps/web/AGENTS.md` | Web-specific invariants (routing, components, styling) |
| `packages/shared/AGENTS.md` | What's generated, what's hand-written, what not to touch |

---

## Universal Commands

```bash
# Install (always after pulling or changing deps)
pnpm install

# Start infra (MongoDB + Redis via Docker)
pnpm infra

# Start everything (infra → migrator → scheduler → api + web)
pnpm start

# Dev mode (with Turborepo — runs migrate, schedule, then dev for all apps)
pnpm turbo-start

# Build all
pnpm turbo build
pnpm install # after pulling or changing deps
pnpm infra # start MongoDB + Redis via Docker
pnpm start # start everything (infra → migrator → scheduler → api + web)
pnpm turbo-start # dev mode via Turborepo (assumes infra running)

pnpm --filter api tsc --noEmit # typecheck API
pnpm --filter web tsc --noEmit # typecheck web
pnpm --filter api build:types # rebuild API declarations (REQUIRED after procedure/schema changes)
```

# Typecheck (per-package)
pnpm --filter api tsc --noEmit
pnpm --filter web tsc --noEmit
---

# Lint (per-package)
pnpm --filter api eslint .
pnpm --filter web eslint .
## Never Do

# Regenerate shared typed client (REQUIRED after any API endpoint/schema change)
pnpm --filter shared generate
```
- Use npm or yarn. pnpm ≥9.5.0 only.
- Use Node < 22.13.0. See `.nvmrc`.
- Create a web page without `.page.tsx` extension.
- Skip the `<Page>` wrapper in web pages.
- Use `src/...` prefix in API imports — `tsconfig.baseUrl` is `src`.
- Add env vars without updating the Zod config schema.
- Use deep relative imports in API procedure files that cross into non-relative modules.
- Forget to update barrels after adding a new endpoint file — add the export to `endpoints/index.ts`. For new resources, also add to `src/router.ts`.

---

## Never Do
## Type Flow: API → Web

- **Use npm or yarn.** `engines` block rejects them. pnpm ≥9.5.0 only.
- **Use Node < 22.13.0.** See `.nvmrc`.
- **Hand-edit `packages/shared/src/generated/`** or `packages/shared/src/schemas/`. These are overwritten by codegen.
- **Forget to run codegen** after changing any `*.schema.ts` or `endpoints/*.ts` in the API.
- **Register routes manually.** Endpoint auto-discovery handles it — just put files in `resources/<name>/endpoints/`.
- **Create a web page without `.page.tsx` extension.** Next.js config only recognizes `*.page.tsx` and `*.api.ts`.
- **Skip the `<Page>` wrapper** in web pages. Every page needs `<Page scope={...} layout={...}>`.
- **Import from `src/...`** in API code. `tsconfig.baseUrl` is `src`, so use `'resources/...'`, `'routes/...'`, `'config'`, etc.
- **Use Zod 3 API.** This repo uses Zod 4 (e.g., `z.email()` not `z.string().email()`).
- **Add env vars without updating the Zod config schema** in `apps/api/src/config/` or `apps/web/src/config/`.
1. API endpoints define `.input(zodSchema).output(zodSchema).handler(...)`.
2. `pnpm --filter api build:types` builds `.d.ts` files to `apps/api/dist/`.
3. Web imports types via `"api": "workspace:*"` dependency — `import type { AppClient } from 'api'`.
4. No codegen, no shared package. Types flow through TypeScript declarations.

---

## Definition of Done

Every change must pass before submission:

- [ ] `pnpm --filter <affected-package> tsc --noEmit` — no type errors
- [ ] `pnpm --filter <affected-package> eslint .` — no lint errors
- [ ] If API endpoints/schemas changed → `pnpm --filter shared generate` ran and output committed
- [ ] If API endpoints/schemas changed → `pnpm --filter api build:types` ran
- [ ] If new env vars → added to `.env.example` AND the Zod config schema
- [ ] Spot-check: the feature works (dev server loads, endpoint responds, page renders)

---

## Self-Maintenance

Update **this file** when: monorepo structure changes, new packages added, universal commands change, or new "never do" items discovered.

Update **scoped files** (`apps/*/AGENTS.md`, `packages/shared/AGENTS.md`) when the invariants they document change.

Update **`agent_docs/*`** when workflows, checklists, or failure modes change. Each doc lists its own update triggers.
Loading
Loading