neo4j-graphql-skill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen to Use
适用场景
- Creating a GraphQL API from a Neo4j graph schema with
@neo4j/graphql - Writing type definitions with ,
@relationship,@cypherdirectives@authorization - Using OGM for server-side programmatic Neo4j access (bypasses GraphQL auth)
- Configuring auto-generated queries, mutations, subscriptions
- Securing types/fields with JWT or JWKS-based rules
@authorization - Migrating from v5/v6 to v7 (breaking changes below)
- 使用从Neo4j图模式创建GraphQL API
@neo4j/graphql - 编写带有、
@relationship、@cypher指令的类型定义@authorization - 使用OGM实现服务器端编程式Neo4j访问(绕过GraphQL权限验证)
- 配置自动生成的查询、变更、订阅
- 使用基于JWT或JWKS的规则保护类型/字段
@authorization - 从v5/v6迁移到v7(下方列出破坏性变更)
When NOT to Use
不适用场景
- Raw Cypher queries outside GraphQL resolvers →
neo4j-cypher-skill - Spring Data Neo4j / Java entity mapping →
neo4j-spring-data-skill - Generic GraphQL without Neo4j — outside scope
- 解析器之外的原生Cypher查询 → 使用
neo4j-cypher-skill - Spring Data Neo4j / Java实体映射 → 使用
neo4j-spring-data-skill - 不涉及Neo4j的通用GraphQL开发 —— 超出本范围
Version Matrix
版本矩阵
| Version | Status | Notes |
|---|---|---|
| v7 | Current | |
| v5 | LTS | Older syntax; |
Default to v7 unless codebase is on v5.
| 版本 | 状态 | 说明 |
|---|---|---|
| v7 | 当前版本 | 必须使用 |
| v5 | 长期支持版 | 旧语法; |
除非代码库使用v5版本,否则默认使用v7。
Step 1 — Install
步骤1 — 安装
bash
npm install @neo4j/graphql neo4j-driver graphql @apollo/serverFor subscriptions (CDC required):
bash
npm install ws graphql-ws express body-parser corsbash
npm install @neo4j/graphql neo4j-driver graphql @apollo/server如需订阅功能(需启用CDC):
bash
npm install ws graphql-ws express body-parser corsStep 2 — Minimal Server Setup
步骤2 — 最小化服务器配置
javascript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';
const typeDefs = `#graphql
type Movie @node {
id: ID! @id
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}
type Person @node {
id: ID! @id
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
`;
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
// assertIndexesAndConstraints syncs @id → UNIQUE constraints; wrap in try/catch
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });
const server = new ApolloServer({ schema: await neoSchema.getSchema() });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
listen: { port: 4000 },
});assertIndexesAndConstraints{ create: true }CREATE CONSTRAINTjavascript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';
const typeDefs = `#graphql
type Movie @node {
id: ID! @id
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}
type Person @node {
id: ID! @id
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
`;
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
// assertIndexesAndConstraints会同步@id到UNIQUE约束;需包裹在try/catch中
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });
const server = new ApolloServer({ schema: await neoSchema.getSchema() });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
listen: { port: 4000 },
});assertIndexesAndConstraints{ create: true }CREATE CONSTRAINTKey Directives
核心指令
@node (v7 required)
@node(v7必填)
Every GraphQL type representing a Neo4j node must have . Without it, v7 ignores the type.
@nodegraphql
type Product @node {
id: ID! @id
name: String!
}每个代表Neo4j节点的GraphQL类型必须添加。如果没有,v7会忽略该类型。
@nodegraphql
type Product @node {
id: ID! @id
name: String!
}Custom label (default = type name)
自定义标签(默认值为类型名称)
type Article @node(labels: ["Post", "Content"]) {
title: String!
}
undefinedtype Article @node(labels: ["Post", "Content"]) {
title: String!
}
undefined@relationship — Full Syntax
@relationship — 完整语法
graphql
type Person @node {
# direction: OUT = (this)-[:KNOWS]->(other)
friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)
# direction: IN = (other)-[:ACTED_IN]->(this)
actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)
# direction: UNDIRECTED = matches both directions (use sparingly — double-counts)
colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)
# Relationship with properties — reference an @relationshipProperties interface
reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}
interface ReviewedProps @relationshipProperties {
rating: Int!
date: Date
}Direction rule: = arrow leaves this node. = arrow enters this node. Both sides of a relationship must declare opposite directions.
OUTINgraphql
type Person @node {
# direction: OUT = (this)-[:KNOWS]->(other)
friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)
# direction: IN = (other)-[:ACTED_IN]->(this)
actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)
# direction: UNDIRECTED = 匹配双向关系(谨慎使用——会重复计数)
colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)
# 带属性的关系——引用@relationshipProperties接口
reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}
interface ReviewedProps @relationshipProperties {
rating: Int!
date: Date
}方向规则: = 箭头从当前节点出发。 = 箭头指向当前节点。关系的两端必须声明相反的方向。
OUTINQuerying Relationship Properties — Connection API
查询关系属性 — Connection API
For each relationship with , a field is auto-generated. Access rel properties via , not via :
properties:{field}ConnectionactorsConnection.edges.propertiesactorsgraphql
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actorsConnection {
edges {
properties { role } # maps to @relationshipProperties interface
node { name }
}
}
}
}对于每个带有的关系,会自动生成字段。需通过访问关系属性,而非:
properties:{field}ConnectionactorsConnection.edges.propertiesactorsgraphql
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actorsConnection {
edges {
properties { role } # 映射到@relationshipProperties接口
node { name }
}
}
}
}@cypher — Custom Resolver
@cypher — 自定义解析器
graphql
type Person @node {
name: String!
# columnName must exactly match the RETURN alias — mismatch returns null silently
friendCount: Int
@cypher(
statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
columnName: "friendCount"
)
recommendedMovies: [Movie!]!
@cypher(
statement: """
MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
WHERE NOT (this)-[:WATCHED]->(rec)
RETURN rec
"""
columnName: "rec"
)
}graphql
type Person @node {
name: String!
# columnName必须与RETURN别名完全匹配——不匹配会静默返回null
friendCount: Int
@cypher(
statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
columnName: "friendCount"
)
recommendedMovies: [Movie!]!
@cypher(
statement: """
MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
WHERE NOT (this)-[:WATCHED]->(rec)
RETURN rec
"""
columnName: "rec"
)
}@cypher on Query field — custom top-level query
@cypher用于Query字段——自定义顶级查询
type Query {
topRatedMovies(limit: Int = 10): [Movie!]!
@cypher(
statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
columnName: "m"
)
}
`this` refers to the current node in field-level @cypher. Parameters are passed as `$paramName`.type Query {
topRatedMovies(limit: Int = 10): [Movie!]!
@cypher(
statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
columnName: "m"
)
}
`this`在字段级@cypher中代表当前节点。参数通过`$paramName`传递。@cypher — Field Arguments and extend type
@cypher — 字段参数与扩展类型
graphql
undefinedgraphql
undefinedextend type adds computed fields without modifying the base type definition
extend type用于添加计算字段,无需修改基础类型定义
extend type Movie @node {
avgRating: Float
@cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")
Field arguments passed as Cypher params; always provide default to avoid null
recommended(limit: Int = 3): [Movie!]!
@cypher(
statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
columnName: "rec"
)
}
undefinedextend type Movie @node {
avgRating: Float
@cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")
字段参数作为Cypher参数传递;始终提供默认值以避免null
recommended(limit: Int = 3): [Movie!]!
@cypher(
statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
columnName: "rec"
)
}
undefined@id and @timestamp
@id和@timestamp
graphql
type Post @node {
id: ID! @id # auto-generates UUID; creates UNIQUE constraint
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
title: String!
}graphql
type Post @node {
id: ID! @id # 自动生成UUID;创建UNIQUE约束
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
title: String!
}@alias — Map GraphQL field to Neo4j property
@alias — 将GraphQL字段映射到Neo4j属性
graphql
type User @node {
id: ID! @id
email: String! @alias(property: "emailAddress") # GraphQL: email → DB: emailAddress
}graphql
type User @node {
id: ID! @id
email: String! @alias(property: "emailAddress") # GraphQL: email → 数据库: emailAddress
}Security — @authentication and @authorization
安全验证 — @authentication和@authorization
Step 1: Configure JWT in constructor
步骤1:在构造函数中配置JWT
javascript
// Symmetric secret
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: { key: process.env.JWT_SECRET },
},
});
// JWKS endpoint (production)
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: {
key: { url: 'https://myapp.com/.well-known/jwks.json' },
},
},
});javascript
// 对称密钥
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: { key: process.env.JWT_SECRET },
},
});
// JWKS端点(生产环境)
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: {
key: { url: 'https://myapp.com/.well-known/jwks.json' },
},
},
});Step 2: Pass token in context
步骤2:在上下文传递token
javascript
context: async ({ req }) => ({ token: req.headers.authorization }),
// Or pass pre-decoded JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),javascript
context: async ({ req }) => ({ token: req.headers.authorization }),
// 或传递预解码的JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),Step 3: Apply @authentication and @authorization
步骤3:应用@authentication和@authorization
graphql
undefinedgraphql
undefinedRequire auth on all operations for a type
对类型的所有操作要求身份验证
type Post @node
@authentication
@authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
title: String!
author: User! @relationship(type: "AUTHORED", direction: IN)
}
type Post @node
@authentication
@authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
title: String!
author: User! @relationship(type: "AUTHORED", direction: IN)
}
requireAuthentication: false = allow public access without JWT
requireAuthentication: false = 允许无需JWT的公共访问
type Article @node
@authorization(filter: [
{ requireAuthentication: false, where: { node: { published: { eq: true } } } }
{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
]) {
title: String!
published: Boolean!
}
type Article @node
@authorization(filter: [
{ requireAuthentication: false, where: { node: { published: { eq: true } } } }
{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
]) {
title: String!
published: Boolean!
}
validate (throws error) vs filter (silently hides data)
validate(抛出错误)vs filter(静默隐藏数据)
type BankAccount @node
@authorization(validate: [{
when: [BEFORE],
where: { node: { owner: { id: { eq: "$jwt.sub" } } } }
}]) {
balance: Float!
}
type BankAccount @node
@authorization(validate: [{
when: [BEFORE],
where: { node: { owner: { id: { eq: "$jwt.sub" } } } }
}]) {
balance: Float!
}
Role-based with custom JWT claims
基于角色的自定义JWT声明
type JWT @jwt {
roles: [String!]! @jwtClaim(path: "myApp.roles")
}
type AdminReport @node
@authentication(operations: [READ], jwt: { roles: { includes: "admin" } }) {
data: String!
}
**filter vs validate**: `filter` silently removes unauthorized data. `validate` throws an error. Use `validate` when data existence should not be revealed to unauthorized users.
**BEFORE vs AFTER**: `CREATE` supports only `AFTER`; `READ` supports only `BEFORE`.
---type JWT @jwt {
roles: [String!]! @jwtClaim(path: "myApp.roles")
}
type AdminReport @node
@authentication(operations: [READ], jwt: { roles: { includes: "admin" } }) {
data: String!
}
**filter与validate的区别**:`filter`会静默移除未授权数据。`validate`会抛出错误。当不应向未授权用户透露数据存在性时,使用`validate`。
**BEFORE与AFTER**:`CREATE`仅支持`AFTER`;`READ`仅支持`BEFORE`。
---Auto-Generated Operations
自动生成的操作
For each type, the library generates:
@node| Operation | Generated Name | Example |
|---|---|---|
| Query all | | |
| Cursor pagination | | |
| Create | | |
| Update | | |
| Delete | | |
对于每个类型,库会自动生成以下操作:
@node| 操作 | 生成名称 | 示例 |
|---|---|---|
| 查询所有 | | |
| 游标分页 | | |
| 创建 | | |
| 更新 | | |
| 删除 | | |
v7 Filter Syntax (explicit eq
)
eqv7过滤语法(显式eq
)
eqgraphql
undefinedgraphql
undefinedv7: explicit eq required
v7: 必须显式使用eq
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actors { name }
}
}
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actors { name }
}
}
Sort and paginate (v7: direct args, not options wrapper)
排序和分页(v7: 直接传参,而非options包装器)
query {
movies(sort: [{ title: ASC }], limit: 10, offset: 0) {
title
}
}
undefinedquery {
movies(sort: [{ title: ASC }], limit: 10, offset: 0) {
title
}
}
undefinedNested Mutations
嵌套变更
graphql
mutation {
createMovies(input: [{
title: "Inception"
actors: {
create: [{ node: { name: "Leonardo DiCaprio" } }]
connect: { where: { node: { name: { eq: "Joseph Gordon-Levitt" } } } }
}
}]) {
movies { id title }
}
}connectOrCreateconnectcreategraphql
mutation {
createMovies(input: [{
title: "Inception"
actors: {
create: [{ node: { name: "Leonardo DiCaprio" } }]
connect: { where: { node: { name: { eq: "Joseph Gordon-Levitt" } } } }
}
}]) {
movies { id title }
}
}connectOrCreateconnectcreateOGM — Programmatic Access
OGM — 编程式访问
OGM bypasses GraphQL authorization — use only in trusted server-side contexts.
javascript
import { OGM } from '@neo4j/graphql-ogm';
const ogm = new OGM({ typeDefs, driver });
await ogm.init(); // must await before using models
const Movie = ogm.model('Movie');
// find
const movies = await Movie.find({
where: { title: { eq: 'The Matrix' } },
selectionSet: `{ id title actors { name } }`,
});
// create
const { movies: created } = await Movie.create({
input: [{ title: 'Dune', actors: { create: [{ node: { name: 'Timothée Chalamet' } }] } }],
});
// update
await Movie.update({
where: { id: { eq: movieId } },
update: { title: { set: 'Dune: Part One' } },
});
// delete
await Movie.delete({ where: { id: { eq: movieId } } });Install separately:
npm install @neo4j/graphql-ogmOGM会绕过GraphQL权限验证——仅在受信任的服务器端上下文使用。
javascript
import { OGM } from '@neo4j/graphql-ogm';
const ogm = new OGM({ typeDefs, driver });
await ogm.init(); // 使用模型前必须等待初始化完成
const Movie = ogm.model('Movie');
// 查询
const movies = await Movie.find({
where: { title: { eq: 'The Matrix' } },
selectionSet: `{ id title actors { name } }`,
});
// 创建
const { movies: created } = await Movie.create({
input: [{ title: 'Dune', actors: { create: [{ node: { name: 'Timothée Chalamet' } }] } }],
});
// 更新
await Movie.update({
where: { id: { eq: movieId } },
update: { title: { set: 'Dune: Part One' } },
});
// 删除
await Movie.delete({ where: { id: { eq: movieId } } });需单独安装:
npm install @neo4j/graphql-ogmSubscriptions (CDC Required)
订阅功能(需启用CDC)
Requires Neo4j CDC enabled in mode. See CDC docs.
FULLjavascript
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: { subscriptions: true },
});Subscriptions auto-generate for each type:
graphql
subscription {
movieCreated(where: { title: { eq: "The Matrix" } }) {
createdMovie { title }
}
}要求Neo4j CDC以模式启用。请查看CDC文档。
FULLjavascript
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: { subscriptions: true },
});每个类型会自动生成订阅:
graphql
subscription {
movieCreated(where: { title: { eq: "The Matrix" } }) {
createdMovie { title }
}
}Also: movieUpdated, movieDeleted
还有:movieUpdated, movieDeleted
---
---Schema Control Directives
模式控制指令
graphql
type ReadOnlyData @node @mutation(operations: []) { value: String! } # disable mutations
type HeavyDoc @node {
id: ID! @id
content: String! @filterable(byValue: false) @sortable(enabled: false) # perf guard
title: String!
}
type Series @node @plural(value: "seriesList") { title: String! } # irregular plural fixgraphql
type ReadOnlyData @node @mutation(operations: []) { value: String! } # 禁用变更操作
type HeavyDoc @node {
id: ID! @id
content: String! @filterable(byValue: false) @sortable(enabled: false) # 性能防护
title: String!
}
type Series @node @plural(value: "seriesList") { title: String! } # 修复不规则复数Common Errors
常见错误
| Error | Cause | Fix |
|---|---|---|
| Missing | Add |
| | Match |
| Relationship direction mismatch | Both sides declare same direction | Inverse: if A has |
| | Add |
| Auth not applied | JWT not in context | Pass |
| 0 results with valid data | v7 filter missing | Use |
| Removed in v7 | Use |
| Memory errors on large mutations | Complex Cypher generation | Batch mutations; increase |
| v7 requires explicit enable | Add |
| 错误 | 原因 | 修复方法 |
|---|---|---|
| 类型缺少 | 为每个节点类型添加 |
| | 确保 |
| 关系方向不匹配 | 两端声明了相同的方向 | 反转方向:如果A使用 |
| | 添加 |
| 权限验证未生效 | 上下文未传递JWT | 在上下文函数中传递 |
| 数据有效但返回0条结果 | v7过滤缺少 | 使用 |
| v7中已移除 | 分别使用 |
| 大型变更时内存错误 | Cypher生成过于复杂 | 分批执行变更;增大 |
| v7需要显式启用 | 在构造函数中添加 |
v6 → v7 Breaking Changes Summary
v6 → v7破坏性变更汇总
| v6 | v7 |
|---|---|
| |
| |
| |
| Removed — use |
| |
Single rel fields | Must use list |
| Removed |
| Removed |
| v6 | v7 |
|---|---|
| 每个节点类型必须添加 |
| |
| |
| 已移除——使用 |
查询中的 | 在 |
单个关系字段 | 必须使用列表 |
| 已移除 |
| 已移除 |
References
参考资料
- Neo4j GraphQL Docs — full directive reference, migration guides
- GraphAcademy: GraphQL Basics — hands-on course
- GitHub: @neo4j/graphql — changelog, issues
- CDC Setup — required for subscriptions
- Neo4j GraphQL文档 —— 完整指令参考、迁移指南
- GraphAcademy: GraphQL基础 —— 实操课程
- GitHub: @neo4j/graphql —— 更新日志、问题反馈
- CDC配置 —— 订阅功能必备
Checklist
检查清单
- on every GraphQL type representing a Neo4j node (v7 hard requirement)
@node - on identity fields (triggers
@idviaCREATE CONSTRAINT)assertIndexesAndConstraints - called on startup with try/catch
assertIndexesAndConstraints - direction correct:
@relationship= arrow leaves this node,OUT= arrow entersIN - Both sides of relationship declared with inverse directions
-
@cyphermatches RETURN alias exactlycolumnName - JWT secret or JWKS URL in ; token passed in context
features.authorization.key - filter vs validate chosen deliberately (silent hide vs thrown error)
@authorization - v7: filters use explicit syntax
{ field: { eq: value } } - v7: /
limitpassed as direct query args (notsortwrapper)options - OGM: called before any
await ogm.init()usageogm.model() - Subscriptions: CDC enabled in FULL mode before enabling
features.subscriptions - holds credentials;
.envin.env.gitignore
- 每个代表Neo4j节点的GraphQL类型都添加了(v7硬性要求)
@node - 标识字段添加了(通过
@id触发assertIndexesAndConstraints)CREATE CONSTRAINT - 启动时调用并包裹在try/catch中
assertIndexesAndConstraints - 方向正确:
@relationship= 箭头从当前节点出发,OUT= 箭头指向当前节点IN - 关系两端声明了相反的方向
- 的
@cypher与RETURN别名完全匹配columnName - 在中配置了JWT密钥或JWKS URL;上下文传递了token
features.authorization.key - 谨慎选择的filter或validate(静默隐藏 vs 抛出错误)
@authorization - v7:过滤使用显式语法
{ field: { eq: value } } - v7:/
limit作为查询直接参数传递(而非sort包装器)options - OGM:在调用任何前执行
ogm.model()await ogm.init() - 订阅功能:在启用前,已将CDC配置为FULL模式
features.subscriptions - 凭证存储在中;
.env已加入.env.gitignore