@gaonjs/adapter-mongo
v0.5.6
Published
Gaon 선택형 문서형(MongoDB) 어댑터 — collection()·mongoSchema() (Mongoose 기반 · SQL model() 과 나란한 문서형 축). mongoose 는 optional peer 로 opt-in.
Readme
@gaonjs/adapter-mongo
Gaon 선택형 문서형(MongoDB) 어댑터 — SQL model()(Kysely) 옆에 문서형 동사
collection() 을 Mongoose 위에 둔다 (v1.1 · 결정 278~282).
로그·이벤트·감사·분석 이벤트처럼 문서·유연 스키마·대량 append 용도에 쓴다.
운영 데이터(관계형·트랜잭션)는 그대로 SQL model() 을 쓴다.
설치 (opt-in)
문서형은 선택 기능이라 SQL 전용 프로젝트는 mongoose 를 받지 않는다(optional peer).
켜려면 mongoose 하나만 받으면 된다:
npm i mongoose@gaonjs/adapter-mongo 자체는 gaonjs·@gaonjs/config 의 정규 dependency 라 이미
설치돼 있다 — 따로 받지 않는다(결정 422 · createDb 의 수리 안내와 같은 문구).
사용
import { collection, mongoSchema } from 'gaonjs/data' // 파사드 재수출
import type { Model } from 'mongoose'
// 정본 패턴(결정 288·332): Doc + Methods + Model 인터페이스 3-제네릭 —
// AuditLog.recent(50)·doc.summary() 가 캐스트 없이 컴파일된다.
interface AuditDoc {
actorId: string
action: string
ip?: string
createdAt?: Date
updatedAt?: Date
}
interface AuditMethods {
summary(): string
}
interface AuditModel extends Model<AuditDoc, {}, AuditMethods> {
recent(limit: number): Promise<AuditDoc[]>
}
const auditLogSchema = mongoSchema<AuditDoc, AuditModel, AuditMethods>({
actorId: { type: String, required: true, index: true },
action: { type: String, required: true },
ip: { type: String, hidden: true }, // 직렬화 경계에서 제외 (SQL .hidden() 과 동일 의미)
}, {
timestamps: true,
statics: {
async recent(limit: number) {
return (await this.find().sort({ createdAt: -1 }).limit(limit).lean()) as AuditDoc[]
},
},
methods: {
summary() { return `${this.actorId}:${this.action}` },
},
})
// db: 'logs' — 몽고 커넥션 바인딩 (SQL 의 { db: 'legacy' } 와 대칭)
export const AuditLog = collection('audit_logs', auditLogSchema, { db: 'logs' })gaon.config.ts:
export default defineConfig({
db: {
main: { adapter: 'postgres', url: env('DATABASE_URL') },
logs: { adapter: 'mongodb', url: env('MONGO_URL') }, // 문서형 커넥션
},
})v1 스코프
- ✅ 순수 CRUD + Mongoose 미들웨어(pre/post) 통과, statics/methods(순수 Mongoose)
- ✅
{ db }커넥션 바인딩 · per-connection 격리(전역mongoose.model()금지) - ✅
hidden필드 직렬화 비노출(SQL 과 동일 — 서버는 읽고 직렬화만 제외 ·.lean()/create()반환/toObject()전부 방어 · 결정 289 · 중첩 dot-path 지원 (profile.ssn·서브도큐먼트 배열 요소 필드 · 결정 336 — 지원 밖 위치는 fail-loud)) - ✅ statics/methods 타입 통로 — Doc+Methods+Model 인터페이스 3-제네릭 정본(결정 332 · 캐스트 0)
- ✅ ObjectId(
_id포함) → string 정규화(응답 경계) - ✅ 인덱스 = Mongoose 스키마 선언 — dev·test 는
autoIndex기본값이 첫 사용 시 보장 · 운영(NODE_ENV=production)은 autoIndex 기본 false(결정 333) — 배포 절차에서syncMongoIndexes()로 명시 동기화(커넥션 설정autoIndex명시가 우선) - ❌ 몽고 마이그레이션 없음(
gaon db순회는 mongodb 를 건너뛰고 skipped 보고 · 결정 292) · 몽고 트랜잭션 없음 · change stream 없음
⚠️
adapter: 'mongodb'커넥션에는url(경로에 DB 명 포함) 또는database가 필수다 — 둘 다 없으면 부팅이 수리 안내로 throw 한다(결정 335 · 드라이버 기본testDB 무신호 접속 봉합).⚠️ 같은 커넥션·같은 컬렉션명에 다른 스키마를 선언하면 첫 접근에서 throw 한다(결정 334) — 같은 컬렉션이면 mongoSchema 산출 하나를 export 해 재사용하라.
크로스커넥션 안전
몽고는 v1 에서 트랜잭션이 없다. SQL service() 트랜잭션 안에서 몽고에 쓰면
MongoCrossConnectionWriteError 로 막힌다(조용한 부분 커밋 방지 · 결정 281·331).
"커밋 후 로그" 는 afterCommit 으로:
service(async () => {
const user = await Users.create(input) // SQL tx
afterCommit(() => AuditLog.create({ actorId: String(user.id), action: 'signup' })) // 커밋 뒤 몽고 쓰기
})⚠️
aggregate결과는 임의 projection 이라hidden마커를 심지 않는다(raw 탈출구). 문서를 렌더에 흘릴 땐.lean()을 권장한다(정본 경로 · 경량).⚠️ 가드는 allowlist 반전이다(결정 331) — SQL tx 안에서는 읽기 전용 내장 (find/findOne/countDocuments/aggregate 읽기 등)만 통과하고, 그 밖의 static 호출 (쓰기·커스텀 static·미지의 신규 메서드)은 fail-closed 로 막힌다. Query 체이닝 쓰기 (
find(f).updateMany(u)등)와aggregate의$out/$merge도 실행 지점 pre 훅이 막는다. 유일한 예외 = 인스턴스doc.save()(정본 예외 · 미가드).
자세한 설계는 docs/design/mongo-collection-adapter.md 및 agents/data.md 참조.
