nodejs-store 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -24,6 +24,7 @@
24
24
  const crud = require('./crud');
25
25
  const datasource = require('./datasource');
26
26
  const executors = require('./executors');
27
+ const feedback = require('./feedback');
27
28
  const introspect = require('./introspect');
28
29
  const permission = require('./permission');
29
30
  const schema = require('./schema');
@@ -51,6 +52,7 @@ class Store {
51
52
  /**
52
53
  * GQL 查询。`routeOverride`(可选):`{ source?, namespace? }` 多租户路由,
53
54
  * 覆盖命令定位(权限/计算列仍按结构 schema 判定)。下同。
55
+ * 注意:`routeOverride` 为**受信服务端参数**,禁止透传用户输入(否则可被用于跨源路由,CWE-639)。
54
56
  */
55
57
  async query(gql, params, routeOverride) {
56
58
  return crud.query(gql, params, routeOverride);
@@ -106,11 +108,6 @@ class Store {
106
108
  return crud.upsert(schemaName, condition, data, options, routeOverride);
107
109
  }
108
110
 
109
- // ── 原生聚合 ──
110
- async aggregate(schemaName, pl, routeOverride) {
111
- return crud.aggregate(schemaName, pl, routeOverride);
112
- }
113
-
114
111
  // ── 结构同步(SQL 数据源:introspect → schemaFromRows → mergeSchema → register) ──
115
112
  async syncSchema(opts) {
116
113
  return syncSchema(opts);
@@ -122,6 +119,30 @@ class Store {
122
119
  return schema.core.buildPipeline(gql, params ?? {}, permission.getContext() ?? null);
123
120
  }
124
121
 
122
+ // ── 宿主接入守卫(Registry 级,对齐 py-store c44001e) ──
123
+ /**
124
+ * 开关「上下文强制」(默认关闭 = fail-open)。开启后:所有查询/写入在 ctx 缺失时
125
+ * 抛 `ERR_NO_CONTEXT`(fail-secure);内部调用须显式传 `{ internal: true }` 上下文。
126
+ */
127
+ setRequireContext(needCtx = true) {
128
+ return schema.setRequireContext(needCtx);
129
+ }
130
+
131
+ /** 「上下文强制」开关当前值(对齐 py-store store.require_context) */
132
+ requireContext() {
133
+ return schema.requireContext();
134
+ }
135
+
136
+ /** 设置数据源连接映射(多后端路由;对齐 py-store store.set_connections) */
137
+ setConnections(connections) {
138
+ return datasource.setConnections(connections);
139
+ }
140
+
141
+ /** 注册反馈事件回调(兜底/降级/拦截的统一出口);传 null 恢复默认 stderr */
142
+ setFeedbackSink(fn) {
143
+ return feedback.setSink(fn);
144
+ }
145
+
125
146
  // ── 权限控制(AsyncLocalStorage 上下文) ──
126
147
  setContext(ctx) {
127
148
  return permission.setContext(ctx);
@@ -156,13 +177,10 @@ async function _createIndexesIfNeeded() {
156
177
  for (const name of names) {
157
178
  const s = schema.get(name);
158
179
  // 索引创建是初始化的辅助动作(非命令路由):schema 绑定的 source 暂未在
159
- // 当前连接映射中时跳过,不阻塞 init(命令路由的 fail fast 不在此处)
160
- let db;
161
- try {
162
- db = datasource.dbOfSchema(name); // Mongo 按 (datasource, namespace) 解析;SQL 源返回 null
163
- } catch (e) {
164
- continue;
165
- }
180
+ // 当前连接映射中时软跳过,不阻塞 init;其余配置错误(namespace 形态不匹配等)
181
+ // 按 fail-fast 由 dbOfSchema 上抛,不静默吞掉
182
+ if (!datasource.hasConnection(datasource.sourceOfSchema(name))) continue;
183
+ const db = datasource.dbOfSchema(name); // Mongo 按 (datasource, namespace) 解析;SQL 源返回 null
166
184
  if (!db) continue; // SQL 后端不建索引
167
185
 
168
186
  const coll = db.collection(s.collection);
@@ -171,7 +189,10 @@ async function _createIndexesIfNeeded() {
171
189
  try {
172
190
  existingIndexes = await coll.listIndexes().toArray();
173
191
  } catch (e) {
174
- existingIndexes = [];
192
+ // 只吞服务器错误(集合尚未存在 → NamespaceNotFound 属 MongoServerError);
193
+ // 连接/程序错误按 fail-fast 上抛,不再静默吞掉(对齐 py 侧 PyMongoError 收窄)
194
+ if (e && e.name === 'MongoServerError') existingIndexes = [];
195
+ else throw e;
175
196
  }
176
197
 
177
198
  for (const idx of s.indexes || []) {
@@ -193,7 +214,15 @@ async function _createIndexesIfNeeded() {
193
214
 
194
215
  await coll.createIndex(Object.entries(keys), finalOptions);
195
216
  } catch (e) {
196
- console.error(`[MongoStore] 创建索引失败 ${s.collection}: ${e && e.message ? e.message : e}`);
217
+ // 索引创建失败不阻塞 init(辅助动作),但必须走统一反馈通道:
218
+ // 无 sink 时由 feedback 默认落 stderr(不双份打印),宿主可 setFeedbackSink 接管
219
+ feedback.emit({
220
+ type: 'index_create_failed',
221
+ code: 'indexCreateFailed',
222
+ layer: 'host',
223
+ message: `创建索引失败 ${s.collection}: ${e && e.message ? e.message : e}`,
224
+ hint: '检查该集合的索引定义与连接权限;索引缺失不影响读写,相关查询将退化为全表扫描',
225
+ });
197
226
  }
198
227
  }
199
228
  }
@@ -224,13 +253,14 @@ module.exports = {
224
253
  init,
225
254
  store,
226
255
  Store,
227
- aggregate: crud.aggregate,
228
256
  PermissionError: permission.PermissionError,
257
+ PushdownUnsupportedError: datasource.PushdownUnsupportedError,
229
258
  datasource,
230
259
  schema,
231
260
  permission,
232
261
  crud,
233
262
  executors,
263
+ feedback,
234
264
  introspect,
235
265
  syncSchema,
236
266
  };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * introspect 共享工具(mysql / postgres 复用,避免复制粘贴式重复)
5
+ *
6
+ * 两后端 `_INDEXES` 查询产出的行形态不同(MySQL `nonUnique` 取反、PG `unique` 直取),
7
+ * 由调用方以 `uniqueOf(row)` 参数化该差异;归并逻辑本身逐行一致,抽为单份实现。
8
+ */
9
+
10
+ /**
11
+ * 把 `{table,name,column,...}` 索引行按 `(table, name)` 归并出
12
+ * `{table, name, columns, unique}` 数组(`unique` 取 `uniqueOf(row)` 的 0/1)
13
+ */
14
+ function groupIndexes(rows, uniqueOf) {
15
+ const byKey = new Map();
16
+ for (const r of rows) {
17
+ const key = `${r.table}::${r.name}`;
18
+ let entry = byKey.get(key);
19
+ if (!entry) {
20
+ entry = { table: r.table, name: r.name, columns: [], unique: uniqueOf(r) };
21
+ byKey.set(key, entry);
22
+ }
23
+ entry.columns.push(r.column);
24
+ }
25
+ return [...byKey.values()];
26
+ }
27
+
28
+ module.exports = { groupIndexes };
@@ -7,6 +7,8 @@
7
7
  * 交 core `schemaFromRows` 做纯映射。建议使用只读账号;库名取当前连接的 `DATABASE()`。
8
8
  */
9
9
 
10
+ const { groupIndexes } = require('./_shared');
11
+
10
12
  const _TABLES = `
11
13
  SELECT table_name AS name
12
14
  FROM information_schema.tables
@@ -42,20 +44,8 @@ const _INDEXES = `
42
44
  WHERE table_schema = DATABASE()
43
45
  ORDER BY table_name, index_name, seq_in_index`;
44
46
 
45
- /** 把 `{table,name,nonUnique,column}` 行按索引名归并出 columns 数组 */
46
- function _groupIndexes(rows) {
47
- const byKey = new Map();
48
- for (const r of rows) {
49
- const key = `${r.table}::${r.name}`;
50
- let entry = byKey.get(key);
51
- if (!entry) {
52
- entry = { table: r.table, name: r.name, columns: [], unique: Number(r.nonUnique) ? 0 : 1 };
53
- byKey.set(key, entry);
54
- }
55
- entry.columns.push(r.column);
56
- }
57
- return [...byKey.values()];
58
- }
47
+ // MySQL 行携带 nonUnique(1=非唯一)→ 取反为 unique 标志;归并逻辑共享见 _shared.js
48
+ const _uniqueOf = (r) => (Number(r.nonUnique) ? 0 : 1);
59
49
 
60
50
  async function introspect(driver, { database = null } = {}) {
61
51
  if (!driver || typeof driver.execute !== 'function') {
@@ -91,7 +81,7 @@ async function introspect(driver, { database = null } = {}) {
91
81
  pk: c.columnKey === 'PRI' ? 1 : 0,
92
82
  })),
93
83
  fks,
94
- indexes: _groupIndexes(indexRows),
84
+ indexes: groupIndexes(indexRows, _uniqueOf),
95
85
  };
96
86
  }
97
87
 
@@ -7,6 +7,8 @@
7
7
  * 产出规范化行 JSON,交 core `schemaFromRows` 做纯映射。建议使用只读账号。
8
8
  */
9
9
 
10
+ const { groupIndexes } = require('./_shared');
11
+
10
12
  const _TABLES = `
11
13
  SELECT table_name AS name
12
14
  FROM information_schema.tables
@@ -63,20 +65,8 @@ const _INDEXES = `
63
65
  WHERE ns.nspname = $1 AND NOT ix.indisprimary
64
66
  ORDER BY t.relname, i.relname`;
65
67
 
66
- /** 把 `{table,name,unique,column}` 行按索引名归并出 columns 数组 */
67
- function _groupIndexes(rows) {
68
- const byKey = new Map();
69
- for (const r of rows) {
70
- const key = `${r.table}::${r.name}`;
71
- let entry = byKey.get(key);
72
- if (!entry) {
73
- entry = { table: r.table, name: r.name, columns: [], unique: Number(r.unique) };
74
- byKey.set(key, entry);
75
- }
76
- entry.columns.push(r.column);
77
- }
78
- return [...byKey.values()];
79
- }
68
+ // PG 行已带 unique(0/1)→ 直取;归并逻辑共享见 _shared.js
69
+ const _uniqueOf = (r) => Number(r.unique);
80
70
 
81
71
  async function introspect(driver, opts = {}) {
82
72
  if (!driver || typeof driver.query !== 'function') {
@@ -106,7 +96,7 @@ async function introspect(driver, opts = {}) {
106
96
  pk: Number(c.pk) || 0,
107
97
  })),
108
98
  fks: fks.rows,
109
- indexes: _groupIndexes(indexRows.rows),
99
+ indexes: groupIndexes(indexRows.rows, _uniqueOf),
110
100
  };
111
101
  }
112
102
 
package/src/permission.js CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  const { AsyncLocalStorage } = require('node:async_hooks');
13
13
 
14
+ const { core } = require('./schema');
15
+
14
16
  const _als = new AsyncLocalStorage();
15
17
 
16
18
  /** 设置当前请求的上下文,每次请求开始时调用一次 */
@@ -41,6 +43,47 @@ async function runAsInternal(fn) {
41
43
  return _als.run(next, () => fn());
42
44
  }
43
45
 
46
+ // ─── core 权限方法包装(对齐 py_store.permission 的同名薄包装) ──────
47
+
48
+ /** 接受 schema 对象或 schema 名称 */
49
+ function _model(schema) {
50
+ return typeof schema === 'string' ? schema : schema.name;
51
+ }
52
+
53
+ function canReadSchema(schema, ctx) {
54
+ return core.canRead(_model(schema), ctx ?? null);
55
+ }
56
+
57
+ function canWriteSchema(schema, ctx) {
58
+ return core.canWrite(_model(schema), ctx ?? null);
59
+ }
60
+
61
+ function shouldInjectOwnerCondition(schema, ctx) {
62
+ return core.shouldInjectOwner(_model(schema), ctx ?? null);
63
+ }
64
+
65
+ function mergeOwnerCondition(schema, ctx, condition) {
66
+ const out = core.mergeOwnerCondition(_model(schema), ctx ?? null, condition ?? null);
67
+ // core 在「不注入」时返回 null(无法区分原条件为 null)→ 原样返回入参条件
68
+ return out === null || out === undefined ? condition : out;
69
+ }
70
+
71
+ function getReadableFields(schema, ctx) {
72
+ return core.readableFields(_model(schema), ctx ?? null);
73
+ }
74
+
75
+ function getReadableRelations(schema, ctx) {
76
+ return core.readableRelations(_model(schema), ctx ?? null);
77
+ }
78
+
79
+ function getWritableFields(schema, ctx) {
80
+ return core.writableFields(_model(schema), ctx ?? null);
81
+ }
82
+
83
+ function filterWritableData(schema, ctx, data) {
84
+ return core.filterWritableData(_model(schema), ctx ?? null, data);
85
+ }
86
+
44
87
  // ─── 自定义错误 ──────────────────────────────────────────────
45
88
 
46
89
  class PermissionError extends Error {
@@ -56,5 +99,13 @@ module.exports = {
56
99
  getContext,
57
100
  scopedRoles,
58
101
  runAsInternal,
102
+ canReadSchema,
103
+ canWriteSchema,
104
+ shouldInjectOwnerCondition,
105
+ mergeOwnerCondition,
106
+ getReadableFields,
107
+ getReadableRelations,
108
+ getWritableFields,
109
+ filterWritableData,
59
110
  PermissionError,
60
111
  };
package/src/schema.js CHANGED
@@ -52,6 +52,8 @@ function register(defn) {
52
52
  namespace: defn.namespace || null,
53
53
  idPrefix: defn.idPrefix || '',
54
54
  timestamps: defn.timestamps !== false,
55
+ // 时间戳单位('ms'/'s'/null=不维护);值合法性由 core.register 校验
56
+ timestampUnit: defn.timestamps === 's' ? 's' : (defn.timestamps === false ? null : 'ms'),
55
57
  fields: defn.fields || {},
56
58
  relations: defn.relations || {},
57
59
  computes,
@@ -98,9 +100,24 @@ function list() {
98
100
  return core.list();
99
101
  }
100
102
 
103
+ /**
104
+ * 开关「上下文强制」(默认关闭 = fail-open,与 JS 原版语义一致)。
105
+ *
106
+ * 开启后:所有 plan 入口遇 ctx 缺失抛 `ERR_NO_CONTEXT` 错误(fail-secure);
107
+ * 内部调用(索引创建、归档回填、后台任务等)须显式传 `{ internal: true }` 上下文。
108
+ */
109
+ function setRequireContext(needCtx = true) {
110
+ core.setRequireContext(Boolean(needCtx));
111
+ }
112
+
113
+ /** 「上下文强制」开关当前值(对齐 py_store.schema.require_context) */
114
+ function requireContext() {
115
+ return core.requireContext();
116
+ }
117
+
101
118
  /** 取 asyncFn 计算列实现(fnRef 缺省 = 计算列 key 名) */
102
119
  function getAsyncFn(fnRef) {
103
120
  return _asyncFns[fnRef];
104
121
  }
105
122
 
106
- module.exports = { core, register, get, has, list, getAsyncFn };
123
+ module.exports = { core, register, get, has, list, setRequireContext, requireContext, getAsyncFn };