// Public URL grammar: space-delimited tokens and indexed scalar fields, never JSON. export type QueryRecord = Record const schemas: Record = { 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 () 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 = ( 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 => Object.fromEntries ([...params].filter (([name]) => name === key || name.startsWith (`${key}.`)))