106 行
3.8 KiB
TypeScript
106 行
3.8 KiB
TypeScript
// Public URL grammar: space-delimited tokens and indexed scalar fields, never JSON.
|
|
export type QueryRecord = Record<string, string | number | boolean | null | undefined>
|
|
const schemas: Record<string, string[]> = {
|
|
conditions: ['field', 'operator', 'value', 'end', 'blank'],
|
|
drill: ['field', 'grain', 'value', 'value_type'],
|
|
tag_refs: ['name', 'tag_id', 'external_tag_id', 'negative', 'missing'],
|
|
column_order: ['value', 'value_type'],
|
|
measures: ['key', 'axis', 'value', 'value_type'] }
|
|
|
|
|
|
export const queryList = (value: string | null): string[] =>
|
|
value == null || value === '' ? [] : value.split (' ')
|
|
|
|
|
|
export const queryRecords = (params: URLSearchParams, key: string): QueryRecord[] => {
|
|
const indexed = new Map<number, QueryRecord> ()
|
|
for (const [name, value] of params)
|
|
{
|
|
if (!name.startsWith (`${key}.`)) continue
|
|
const parts = name.slice (key.length + 1).split ('.')
|
|
if (parts.length !== 2 || !/^\d{1,2}$/.test (parts[0])
|
|
|| !schemas[key]?.includes (parts[1])) continue
|
|
const index = Number (parts[0])
|
|
const record = indexed.get (index) ?? { }
|
|
const field = parts[1]
|
|
record[field] = ['blank', 'negative', 'missing'].includes (field)
|
|
? value === '1' : (['tag_id', 'external_tag_id', 'axis'].includes (field)
|
|
? Number (value) : value)
|
|
indexed.set (index, record)
|
|
}
|
|
return [...indexed].sort ((a, b) => a[0] - b[0]).map (([, record]) => {
|
|
if (record.value_type === 'null') record.value = null
|
|
if (record.value_type === 'number') record.value = Number (record.value)
|
|
if (record.value_type === 'boolean') record.value = record.value === 'true'
|
|
delete record.value_type
|
|
return record
|
|
})
|
|
}
|
|
|
|
|
|
export const setQueryRecords = (
|
|
params: URLSearchParams,
|
|
key: string,
|
|
records: QueryRecord[],
|
|
): void => {
|
|
for (const name of [...params.keys ()])
|
|
if (name === key || name.startsWith (`${key}.`)) params.delete (name)
|
|
params.set (key, '')
|
|
records.forEach ((record, index) => {
|
|
for (const [field, value] of Object.entries (record))
|
|
{
|
|
if (value == null && field !== 'value') continue
|
|
if (!schemas[key]?.includes (field)) continue
|
|
const name = `${key}.${index}.${field}`
|
|
if (field === 'value' && ['drill', 'measures', 'column_order'].includes (key))
|
|
params.set (`${key}.${index}.value_type`, value == null ? 'null' : typeof value)
|
|
if (value == null) continue
|
|
params.set (name, typeof value === 'boolean'
|
|
? (['blank', 'negative', 'missing'].includes (field) ? (value ? '1' : '0') : String (value))
|
|
: String (value))
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
export const readCountState = <T,> (
|
|
params: URLSearchParams,
|
|
key: string,
|
|
fallback: T,
|
|
): T => {
|
|
if (!params.has (key)) return fallback
|
|
if (key === 'rows' || key === 'cols')
|
|
return queryList (params.get (key)).map (token => {
|
|
const [field, grain] = token.split (':')
|
|
return grain == null ? { field } : { field, grain }
|
|
}) as T
|
|
return queryRecords (params, key) as T
|
|
}
|
|
|
|
|
|
export const encodeCountState = (key: string, records: QueryRecord[]): string => {
|
|
if (key === 'rows' || key === 'cols')
|
|
return records.map (record => record.grain == null
|
|
? String (record.field) : `${record.field}:${record.grain}`).join (' ')
|
|
const params = new URLSearchParams ()
|
|
setQueryRecords (params, key, records)
|
|
return params.toString ()
|
|
}
|
|
|
|
|
|
export const setCountState = (params: URLSearchParams, key: string, value: string): void => {
|
|
if (schemas[key] != null)
|
|
{
|
|
const fields = new URLSearchParams (value)
|
|
setQueryRecords (params, key, queryRecords (fields, key))
|
|
}
|
|
else params.set (key, value)
|
|
}
|
|
|
|
|
|
export const queryRecordParams = (
|
|
params: URLSearchParams,
|
|
key: string,
|
|
): Record<string, string> => Object.fromEntries ([...params].filter (([name]) =>
|
|
name === key || name.startsWith (`${key}.`)))
|