first commit

This commit is contained in:
Zakaria
2026-09-22 16:13:21 -04:00
commit 7e49be27f1
297 changed files with 63162 additions and 0 deletions
@@ -0,0 +1,230 @@
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
import { azure, type AzureRuntimeClient } from '../azure'
import { azureServerlessSchema } from '../cloud-spi/serverlessSchema'
import type {
CloudResource,
CloudServiceAdapter,
CloudServiceDescriptorOverride,
CreateResourceInput,
ResourceQuery,
ServerlessInvokeResult,
ServiceSchema,
} from '../cloud-spi/types'
interface AzureFunctionRecord {
id?: string
name?: string
kind?: string
location?: string
type?: string
properties?: {
state?: string
status?: string
runtime?: string
functionAppName?: string
lastModifiedTimeUtc?: string
scriptHref?: string
invokeUrlTemplate?: string
config?: Record<string, unknown>
files?: Record<string, string>
}
}
interface AzureFunctionListResponse {
value?: AzureFunctionRecord[]
}
export class AzureServerlessAdapter implements CloudServiceAdapter {
readonly cloud = 'azure' as const
readonly service = 'serverless' as const
constructor(private readonly client: AzureRuntimeClient = azure) { }
/**
* floci-az answers 501 NotImplemented on /functions, so this adapter cannot
* currently serve a request even though it is registered. Reporting
* coming_soon keeps the nav honest; remove this once the runtime ships the
* endpoint and the adapter's own tests exercise it against a real response.
*/
descriptorOverride(): CloudServiceDescriptorOverride {
return {
availability: 'coming_soon',
reason: 'The Floci-AZ runtime returns 501 NotImplemented for the Azure Functions endpoint.',
}
}
schema(): ServiceSchema {
return azureServerlessSchema()
}
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
const body = await this.azureJson<AzureFunctionListResponse | AzureFunctionRecord[]>(
'/functions',
{ method: 'GET' },
{ emptyOnNotFound: true },
)
const records = Array.isArray(body) ? body : body?.value ?? []
return filterBySearch(records.map(toFunctionResource), query.search)
}
async get(id: string): Promise<CloudResource | null> {
const body = await this.azureJson<AzureFunctionRecord>(
`/functions/${encodeURIComponent(id)}`,
{ method: 'GET' },
{ emptyOnNotFound: true },
)
return body ? toFunctionResource(body) : null
}
async create(input: CreateResourceInput): Promise<CloudResource> {
const functionName = stringValue(input.values.functionName ?? input.values.name)
const runtime = stringValue(input.values.runtime) || 'node'
const handler = stringValue(input.values.handler)
const code = stringValue(input.values.code)
const location = stringValue(input.values.location)
const functionAppName = stringValue(input.values.functionAppName)
if (!functionName) throw new ValidationError('functionName is required')
const body = await this.azureJson<AzureFunctionRecord>(
'/functions',
{
method: 'POST',
body: JSON.stringify({
name: functionName,
runtime,
handler: handler || undefined,
code: code || undefined,
location: location || undefined,
functionAppName: functionAppName || undefined,
}),
},
)
if (!body) throw new RuntimeError('Azure Functions create returned an empty response')
return toFunctionResource(body)
}
async delete(id: string): Promise<void> {
await this.client.fetch(
`/functions/${encodeURIComponent(id)}`,
{ method: 'DELETE' },
{ emptyOnNotFound: true },
)
}
async invoke(id: string, payload: string): Promise<ServerlessInvokeResult> {
const startedAt = performance.now()
const body = await this.azureJson<{
statusCode?: number
payload?: unknown
body?: unknown
error?: string
functionError?: string
logResult?: string
}>(
`/functions/${encodeURIComponent(id)}/invoke`,
{
method: 'POST',
body: JSON.stringify({
payload: payload?.trim() ? payload : '{}',
}),
},
)
return {
statusCode: body?.statusCode ?? 200,
payload: stringifyPayload(body?.payload ?? body?.body ?? ''),
functionError: body?.functionError ?? body?.error,
logResult: body?.logResult,
executionDuration: Math.round(performance.now() - startedAt),
}
}
private async azureJson<T>(
path: string,
init: RequestInit,
options?: { emptyOnNotFound?: boolean },
): Promise<T | null> {
const res = await this.client.fetch(
path,
{
...init,
headers: {
accept: 'application/json',
'content-type': 'application/json',
...(init.headers ?? {}),
},
},
options,
)
if (!res || res.status === 204) return null
return await res.json() as T
}
}
function toFunctionResource(record: AzureFunctionRecord): CloudResource {
const name = stringValue(record.name ?? record.id)
const props = record.properties ?? {}
return {
id: name,
name,
cloud: 'azure',
service: 'serverless',
type: 'azure-function',
region: record.location ?? null,
createdAt: props.lastModifiedTimeUtc ?? null,
status: props.state ?? props.status ?? null,
metadata: {
provider: 'azure',
serverlessService: 'functions',
kind: record.kind,
resourceType: record.type,
runtime: props.runtime,
functionAppName: props.functionAppName,
lastModified: props.lastModifiedTimeUtc,
triggerType: getTriggerType(props.config),
scriptHref: props.scriptHref,
invokeUrlTemplate: props.invokeUrlTemplate,
config: props.config,
files: props.files,
},
}
}
function stringValue(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
function stringifyPayload(value: unknown): string {
if (typeof value === 'string') return value
if (value === undefined || value === null) return ''
return JSON.stringify(value)
}
function getTriggerType(config?: Record<string, unknown>): string | undefined {
const bindings = config?.bindings
if (!Array.isArray(bindings)) return undefined
const trigger = bindings.find((binding) => {
if (!binding || typeof binding !== 'object') return false
const type = (binding as Record<string, unknown>).type
return typeof type === 'string' && type.toLowerCase().endsWith('trigger')
})
if (!trigger || typeof trigger !== 'object') return undefined
const type = (trigger as Record<string, unknown>).type
return typeof type === 'string' ? type : undefined
}
function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] {
const normalized = search?.trim().toLowerCase()
if (!normalized) return resources
return resources.filter((resource) => resource.name.toLowerCase().includes(normalized))
}