json-rest-api 1.0.15 → 1.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -184,7 +184,8 @@ export async function compileSchemas (scope, deps) {
184
184
  const schemaContext = {
185
185
  fields: enrichedFields, // Mutable
186
186
  originalFields: rawFields, // Read-only
187
- scopeName
187
+ scopeName,
188
+ scopeOptions: scope.scopeOptions || {}
188
189
  }
189
190
  await runHooks('schema:enrich', schemaContext)
190
191
 
@@ -24,10 +24,16 @@ export const AccessPlugin = {
24
24
 
25
25
  const ownershipField = () => config.ownership.field
26
26
 
27
+ // Minimal helper: treat ownerField false/null as ownership disabled (alias)
28
+ const isOwnershipDisabled = (scopeOptions = {}) => (
29
+ scopeOptions?.ownership === false ||
30
+ scopeOptions?.ownerField === false ||
31
+ scopeOptions?.ownerField === null
32
+ )
33
+
27
34
  const evaluateOwnership = ({ record, field, schemaInfo, userId }) => {
28
35
  if (!record) return 'unknown'
29
36
 
30
- debugger
31
37
  const idProperty = schemaInfo?.idProperty || 'id'
32
38
  const schemaStructure = schemaInfo?.schemaStructure || {}
33
39
  const schemaRelationships = schemaInfo?.schemaRelationships || {}
@@ -146,7 +152,6 @@ export const AccessPlugin = {
146
152
  const auth = context.scopeOptions?.auth
147
153
  if (!auth) return
148
154
 
149
- debugger
150
155
  scope.vars.authRules = auth
151
156
  scope.vars.ownershipField = context.scopeOptions?.ownershipField
152
157
 
@@ -159,11 +164,11 @@ export const AccessPlugin = {
159
164
  })
160
165
  })
161
166
 
162
- addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context, scopeOptions }) => {
167
+ addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context }) => {
163
168
  if (!config.ownership.enabled) return
164
- const { fields, scopeName } = context
169
+ const { fields, scopeName, scopeOptions = {} } = context
165
170
  if (config.ownership.excludeResources.includes(scopeName)) return
166
- if (scopeOptions?.ownership === false) return
171
+ if (isOwnershipDisabled(scopeOptions)) return
167
172
 
168
173
  const field = ownershipField()
169
174
  const relationshipName = fields[field]?.as || fields[field]?.belongsTo || field
@@ -198,7 +203,7 @@ export const AccessPlugin = {
198
203
  return
199
204
  }
200
205
 
201
- if (scopeOptions?.ownership === false) return
206
+ if (isOwnershipDisabled(scopeOptions)) return
202
207
 
203
208
  const field = ownershipField()
204
209
  const scope = scopes[scopeName]
@@ -259,7 +264,7 @@ export const AccessPlugin = {
259
264
  }
260
265
 
261
266
  if (context.auth.roles?.includes?.('admin')) return
262
- if (scopeOptions?.ownership === false) return
267
+ if (isOwnershipDisabled(scopeOptions)) return
263
268
 
264
269
  const field = ownershipField()
265
270
  const scope = scopes[scopeName]
@@ -285,7 +290,6 @@ export const AccessPlugin = {
285
290
 
286
291
  addHook('checkPermissions', 'rest-auth-enforce', { sequence: -100 }, async ({ context, scope, scopeName }) => {
287
292
  const operation = context.method
288
- debugger
289
293
  const scopeVars = scope?.vars
290
294
  const authRules = scopeVars?.authRules
291
295
  const minimalRecord = context.originalContext?.minimalRecord
@@ -300,7 +304,6 @@ export const AccessPlugin = {
300
304
  const failures = []
301
305
 
302
306
  for (const rule of rules) {
303
- // if (rule === 'owns') debugger
304
307
  try {
305
308
  const [checkerName, ...paramParts] = rule.split(':')
306
309
  const checker = state.authCheckers.get(checkerName)
@@ -346,7 +349,7 @@ export const AccessPlugin = {
346
349
  const auth = context.originalContext?.auth || context.auth
347
350
  if (!auth?.userId) return
348
351
  if (auth.roles?.includes?.('admin')) return
349
- if (scopeOptions?.ownership === false) return
352
+ if (isOwnershipDisabled(scopeOptions)) return
350
353
 
351
354
  const field = ownershipField()
352
355
  const schemaInfo = scope?.vars?.schemaInfo || {}
@@ -1872,3 +1872,77 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
1872
1872
 
1873
1873
  return api
1874
1874
  }
1875
+
1876
+
1877
+ /**
1878
+ * Creates an API with resources that OPT OUT of ownership
1879
+ * - public_items_ownerfield: uses ownerField: false
1880
+ * - public_items_ownership: uses ownership: false
1881
+ * Both tables intentionally DO NOT have a user_id column.
1882
+ */
1883
+ export async function createOwnershipOptOutApi (knex, pluginOptions = {}) {
1884
+ const apiName = pluginOptions.apiName || 'ownership-optout-api'
1885
+ const tablePrefix = pluginOptions.tablePrefix || 'ownopt'
1886
+ if (storageMode.isAnyApi()) {
1887
+ storageMode.clearRegistry()
1888
+ }
1889
+
1890
+ const api = new Api({
1891
+ name: apiName,
1892
+ log: { level: process.env.LOG_LEVEL || 'info' }
1893
+ })
1894
+
1895
+ const previousTenant = storageMode.currentTenant
1896
+ const tenantId = storageMode.isAnyApi()
1897
+ ? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
1898
+ : storageMode.defaultTenant
1899
+ if (storageMode.isAnyApi()) {
1900
+ storageMode.setCurrentTenant(tenantId)
1901
+ }
1902
+
1903
+ await api.use(RestApiPlugin, {
1904
+ simplifiedApi: false,
1905
+ simplifiedTransport: false,
1906
+ returnRecordApi: { post: true, put: false, patch: false },
1907
+ returnRecordTransport: { post: 'full', put: 'no', patch: 'minimal' },
1908
+ sortableFields: ['id', 'name']
1909
+ })
1910
+ await useStoragePlugin(api, knex, { tenantId })
1911
+ await resetAnyApiTables(knex)
1912
+
1913
+ try {
1914
+ // Access plugin (defaults: ownership enabled, filterByOwner true)
1915
+ await api.use(AccessPlugin, pluginOptions.access || {})
1916
+
1917
+ // Resource A: disable via ownerField:false
1918
+ await api.addResource('public_items_ownerfield', {
1919
+ schema: {
1920
+ id: { type: 'id' },
1921
+ name: { type: 'string', required: true }
1922
+ },
1923
+ // Important: no user_id column in schema; ownerField disables ownership logic
1924
+ ownerField: false,
1925
+ tableName: `${tablePrefix}_public_items_of`
1926
+ })
1927
+ await api.resources.public_items_ownerfield.createKnexTable()
1928
+ mapTable(`${tablePrefix}_public_items_of`, 'public_items_ownerfield')
1929
+
1930
+ // Resource B: disable via ownership:false
1931
+ await api.addResource('public_items_ownership', {
1932
+ schema: {
1933
+ id: { type: 'id' },
1934
+ name: { type: 'string', required: true }
1935
+ },
1936
+ ownership: false,
1937
+ tableName: `${tablePrefix}_public_items_own`
1938
+ })
1939
+ await api.resources.public_items_ownership.createKnexTable()
1940
+ mapTable(`${tablePrefix}_public_items_own`, 'public_items_ownership')
1941
+
1942
+ return api
1943
+ } finally {
1944
+ if (storageMode.isAnyApi()) {
1945
+ storageMode.setCurrentTenant(previousTenant)
1946
+ }
1947
+ }
1948
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import {
5
+ validateJsonApiStructure,
6
+ cleanTables,
7
+ createJsonApiDocument,
8
+ assertResourceAttributes
9
+ } from './helpers/test-utils.js'
10
+ import { createOwnershipOptOutApi } from './fixtures/api-configs.js'
11
+
12
+ const knex = knexLib({
13
+ client: 'better-sqlite3',
14
+ connection: { filename: ':memory:' },
15
+ useNullAsDefault: true
16
+ })
17
+
18
+ let api
19
+
20
+ async function createPublicItem (resourceName, name, authUserId) {
21
+ const doc = createJsonApiDocument(resourceName, { name })
22
+ const res = await api.resources[resourceName].post({
23
+ inputRecord: doc,
24
+ simplified: false
25
+ }, { auth: { userId: authUserId } })
26
+
27
+ validateJsonApiStructure(res)
28
+ assertResourceAttributes(res.data, { name })
29
+ return res.data
30
+ }
31
+
32
+ describe('Ownership opt-out (ownerField:false / ownership:false)', () => {
33
+ before(async () => {
34
+ api = await createOwnershipOptOutApi(knex)
35
+ })
36
+
37
+ after(async () => {
38
+ await knex.destroy()
39
+ })
40
+
41
+ beforeEach(async () => {
42
+ await cleanTables(knex, [
43
+ 'ownopt_public_items_of',
44
+ 'ownopt_public_items_own'
45
+ ])
46
+ })
47
+
48
+ it('ownerField:false - allows create+query without user_id column present', async () => {
49
+ await createPublicItem('public_items_ownerfield', 'Alpha', 1)
50
+ await createPublicItem('public_items_ownerfield', 'Beta', 2)
51
+
52
+ // Query with auth
53
+ const resultAuth = await api.resources.public_items_ownerfield.query({ simplified: false }, { auth: { userId: 1 } })
54
+ validateJsonApiStructure(resultAuth, true)
55
+ assert.equal(resultAuth.data.length, 2)
56
+
57
+ // Query without auth (ownership disabled -> no filtering)
58
+ const resultNoAuth = await api.resources.public_items_ownerfield.query({ simplified: false })
59
+ validateJsonApiStructure(resultNoAuth, true)
60
+ assert.equal(resultNoAuth.data.length, 2)
61
+ })
62
+
63
+ it('ownership:false - allows create+query without user_id column present', async () => {
64
+ await createPublicItem('public_items_ownership', 'Gamma', 1)
65
+ await createPublicItem('public_items_ownership', 'Delta', 2)
66
+
67
+ const resultAuth = await api.resources.public_items_ownership.query({ simplified: false }, { auth: { userId: 99 } })
68
+ validateJsonApiStructure(resultAuth, true)
69
+ assert.equal(resultAuth.data.length, 2)
70
+
71
+ const resultNoAuth = await api.resources.public_items_ownership.query({ simplified: false })
72
+ validateJsonApiStructure(resultNoAuth, true)
73
+ assert.equal(resultNoAuth.data.length, 2)
74
+ })
75
+ })
76
+