镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit f632cb7

Browse files
committed
fix(dashboard): prefer app models over disabled framework proxies
1 parent b154165 commit f632cb7

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

‎storage/framework/core/actions/tests/fixtures/dashboard-model-access.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ await writeFile(join(project, 'storage/framework/defaults/app/Models/User.ts'),
2121
for (const [name, table, dashboard] of [
2222
['PrivateRecord', 'private_records', { access: false, highlight: true }],
2323
['PublicRecord', 'public_records', { enabled: false, icon: 'i-lucide-book-open' }],
24+
['Coupon', 'custom_coupons', { icon: 'i-lucide-ticket-percent' }],
2425
// Absent private tables prove generic reads/counts/search never reach their database work.
2526
['MissingPrivateRecord', 'missing_private_records', { access: false }],
2627
['User', 'private_users', { access: false }],
@@ -31,6 +32,7 @@ for (const [name, table, dashboard] of [
3132
const sql = new Database(file)
3233
sql.run('CREATE TABLE private_records (id INTEGER PRIMARY KEY, name TEXT)')
3334
sql.run('CREATE TABLE public_records (id INTEGER PRIMARY KEY, name TEXT)')
35+
sql.run('CREATE TABLE custom_coupons (id INTEGER PRIMARY KEY, name TEXT)')
3436
sql.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
3537
sql.run("INSERT INTO users (name) VALUES ('framework-user-must-not-be-counted')")
3638
sql.close()
@@ -43,11 +45,16 @@ const { configureOrm, releaseOrm } = await import('@stacksjs/query-builder')
4345
configureOrm({ database: file })
4446
const Private = (await import(join(project, 'app/Models/PrivateRecord.ts'))).default
4547
const Public = (await import(join(project, 'app/Models/PublicRecord.ts'))).default
48+
const Coupon = (await import(join(project, 'app/Models/Coupon.ts'))).default
49+
const frameworkCoupon = (await import('@stacksjs/orm')).Coupon
50+
assert(!('where' in frameworkCoupon), 'the fixture keeps framework commerce disabled')
51+
Object.assign(globalThis, { Coupon: frameworkCoupon })
4652
Object.assign(globalThis, { PrivateRecord: Private, PublicRecord: Public })
4753
for (const name of ['MissingPrivateRecord', 'User', 'Subscriber'])
4854
Object.assign(globalThis, { [name]: (await import(join(project, `app/Models/${name === 'User' ? 'internal/' : ''}${name}.ts`))).default })
4955
const row = await Private.create({ name: 'private-marker' })
5056
await Public.create({ name: 'public-marker' })
57+
await Coupon.create({ name: 'app-owned-coupon' })
5158
assert.equal((await Private.find(row.id)).name, 'private-marker')
5259
const { ensureFrameworkAuthTables } = await import('../../../auth/tests/helpers/auth-schema')
5360
await ensureFrameworkAuthTables()
@@ -96,6 +103,7 @@ for (const nativeRoutes of [false, true]) {
96103
assert.equal(await Private.find(apiRow.id), undefined)
97104
assert.deepEqual(await (await fetch(`${base}/named`)).json(), { name: 'private-marker' })
98105
assert.equal((await fetch(`${base}/generic/public-record`)).status, 200, 'enabled:false remains presentation only')
106+
assert.equal((await fetch(`${base}/generic/coupon`)).status, 200, 'an app-owned model wins over a disabled framework proxy')
99107
const createdResponse = await fetch(`${base}/generic/public-record`, { method: 'POST', headers: { authorization: 'Bearer fixture-csrf-exemption', 'content-type': 'application/json' }, body: '{"fields":{"name":"public-created"}}' })
100108
assert.equal(createdResponse.status, 200)
101109
const created = await createdResponse.json() as { id: number }
@@ -117,8 +125,9 @@ for (const nativeRoutes of [false, true]) {
117125
const catalogData = JSON.parse(catalog)
118126
assert.equal(catalogData.categoryGroups.flatMap((group: any) => group.models).find((model: any) => model.name === 'PublicRecord').icon, 'i-lucide-book-open', 'the model directory preserves model-defined icons')
119127
assert(!catalogData.categoryGroups.flatMap((group: { models: Array<{ name: string }> }) => group.models).some((model: { name: string }) => model.name === 'User'), 'nested private User override must suppress its framework catalog entry')
120-
assert.equal(catalogData.totalModels, 1)
121-
assert.equal(catalogData.totalRecords, 1)
128+
assert(catalog.includes('Coupon'), 'the catalog includes app-owned commerce models')
129+
assert.equal(catalogData.totalModels, 2)
130+
assert.equal(catalogData.totalRecords, 2)
122131
const legacy = await (await fetch(`${base}/legacy`)).text()
123132
assert(!legacy.includes('PrivateRecord'))
124133
assert(legacy.includes('PublicRecord'))

‎storage/framework/defaults/resources/functions/dashboard/model-access.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** Server-side exposure boundary for the generic dashboard model surfaces. */
22
import type { DashboardModelOptions } from '@stacksjs/types'
3+
import { resolveModelSources } from '@stacksjs/database/model-sources'
34
import { loadModelIfExists } from './data'
45

56
/** Omission preserves the existing dashboard exposure. `enabled` only controls navigation. */
@@ -9,6 +10,13 @@ export function hasDashboardModelAccess(Model: { dashboard?: DashboardModelOptio
910

1011
/** Resolve the native model, then exclude it before callers inspect metadata or query rows. */
1112
export async function resolveDashboardModel(name: string, candidate?: any): Promise<any | null> {
13+
// App-owned definitions retain their native ORM and dashboard access even
14+
// when a same-named framework feature exports a disabled proxy.
15+
const source = resolveModelSources()?.models.find(model => model.name === name)
16+
if (source && source.origin !== 'framework') {
17+
const Model = candidate ?? await loadModelIfExists(name)
18+
return Model && hasDashboardModelAccess(Model) ? Model : null
19+
}
1220
const injected = (globalThis as Record<string, any>)[name]
1321
// A model whose feature is off is still the ORM's export, as a proxy that
1422
// throws on `.where` (#2928). `in` asks it without the throw, and a feature

0 commit comments

Comments
 (0)