rev=N: bumped by developer when function logic changes. Becomes part of the HMAC cache key — old cache entries are unreachable without purge. Effective rev for a context is max(rev) across all functions in it. cache=int|False|True: TTL escape hatch for unobservable mutations. cache=60 emits s-maxage=60. cache=False emits no-store. Default (True) emits s-maxage=31536000 (forever, purge on mutation). Effective cache for a context is min(TTL) across functions, with False taking precedence. Both parameters flow through: decorator → meta → manifest → cache key and Cache-Control headers. Implemented in both Python and TypeScript with 13 Python tests and 4 TypeScript tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
/**
|
|
* Edge Manifest Generator
|
|
*
|
|
* Produces the same JSON format as mizan-django. One Edge Worker.
|
|
* Two backend languages. Same manifest.
|
|
*/
|
|
|
|
import type { EdgeManifest } from './types'
|
|
import { getAllFunctions, getContextGroups, getContextParamNames } from './registry'
|
|
|
|
// Both camelCase and snake_case forms included for cross-language matching.
|
|
// Wire format is snake_case (protocol rule); camelCase is the TS-local convention.
|
|
const USER_SCOPED_PARAMS = new Set(['userId', 'user', 'ownerId', 'accountId', 'user_id', 'owner_id', 'account_id'])
|
|
|
|
export function generateManifest(baseUrl = '/api/mizan'): EdgeManifest {
|
|
const groups = getContextGroups()
|
|
const allFunctions = getAllFunctions()
|
|
const manifest: EdgeManifest = { version: 1, contexts: {}, mutations: {} }
|
|
|
|
// Contexts
|
|
for (const [ctxName, fnNames] of Object.entries(groups)) {
|
|
const paramNames = new Set<string>()
|
|
const functions: Array<{ name: string; path: 'rpc' | 'view'; route?: string; methods?: string[] }> = []
|
|
const pageRoutes: string[] = []
|
|
|
|
for (const fnName of fnNames) {
|
|
const entry = allFunctions.get(fnName)
|
|
if (!entry) continue
|
|
|
|
for (const p of entry.params) paramNames.add(p.name)
|
|
|
|
const fnEntry: any = { name: fnName, path: entry.viewPath ? 'view' : 'rpc' }
|
|
if (entry.route) {
|
|
fnEntry.route = entry.route
|
|
fnEntry.methods = entry.methods || ['GET']
|
|
pageRoutes.push(entry.route)
|
|
}
|
|
if (entry.rev !== undefined && entry.rev !== 0) fnEntry.rev = entry.rev
|
|
if (entry.cache !== undefined && entry.cache !== true) fnEntry.cache = entry.cache
|
|
functions.push(fnEntry)
|
|
}
|
|
|
|
const sortedParams = [...paramNames].sort()
|
|
const userScoped = [...paramNames].some(p => USER_SCOPED_PARAMS.has(p))
|
|
|
|
const ctxEntry: EdgeManifest['contexts'][string] = {
|
|
functions,
|
|
endpoints: [`${baseUrl}/ctx/${ctxName}/`],
|
|
params: sortedParams,
|
|
user_scoped: userScoped,
|
|
render_strategy: userScoped ? 'dynamic_cached' : 'psr',
|
|
}
|
|
|
|
if (pageRoutes.length > 0) {
|
|
ctxEntry.page_routes = pageRoutes
|
|
}
|
|
|
|
manifest.contexts[ctxName] = ctxEntry
|
|
}
|
|
|
|
// Mutations
|
|
for (const [fnName, entry] of allFunctions) {
|
|
if (!entry.affects) continue
|
|
|
|
const affectedContexts = [...new Set(entry.affects.map(a => a.name))]
|
|
|
|
// Auto-scoped params
|
|
const fnParamNames = new Set(entry.params.map(p => p.name))
|
|
const autoScoped: string[] = []
|
|
for (const ctxName of affectedContexts) {
|
|
const ctxParams = getContextParamNames(ctxName)
|
|
for (const p of fnParamNames) {
|
|
if (ctxParams.has(p) && !autoScoped.includes(p)) {
|
|
autoScoped.push(p)
|
|
}
|
|
}
|
|
}
|
|
|
|
const mutation: EdgeManifest['mutations'][string] = {
|
|
affects: affectedContexts,
|
|
}
|
|
if (autoScoped.length > 0) mutation.auto_scoped_params = autoScoped.sort()
|
|
if (entry.private) mutation.private = true
|
|
if (entry.route) {
|
|
mutation.route = entry.route
|
|
mutation.methods = entry.methods || ['POST']
|
|
}
|
|
|
|
manifest.mutations[fnName] = mutation
|
|
}
|
|
|
|
return manifest
|
|
}
|