// 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', 'input', 'values'], condition_values: ['condition', 'value', 'value_type'], 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', 'values'].includes (field) ? value === '1' : (['tag_id', 'external_tag_id', 'axis', 'condition'].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', 'condition_values'].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', 'values'].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 const records = queryRecords (params, key) if (key === 'conditions') { const values = queryRecords (params, 'condition_values') return records.map ((record, index) => record.values === true ? { ...record, values: values.filter (entry => entry.condition === index) .map (entry => entry.value) } : record) as T } return records as T } export const encodeCountState = (key: string, records: Record[]): 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 () const values: QueryRecord[] = [] const flat = records.map ((record, condition) => { if (key !== 'conditions' || !Array.isArray (record.values)) return record as QueryRecord const { values: selected, ...fields } = record for (const value of selected as unknown[]) values.push ({ condition, value: value as string | number | boolean }) return { ...fields, values: true } as QueryRecord }) setQueryRecords (params, key, flat) if (key === 'conditions' && flat.some (record => record.values === true)) setQueryRecords (params, 'condition_values', values) 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)) if (key === 'conditions') { for (const name of [...params.keys ()]) if (name === 'condition_values' || name.startsWith ('condition_values.')) params.delete (name) if (fields.has ('condition_values')) setQueryRecords (params, 'condition_values', queryRecords (fields, 'condition_values')) } } else params.set (key, value) } export const queryRecordParams = ( params: URLSearchParams, key: string, ): Record => Object.fromEntries ([...params].filter (([name]) => name === key || name.startsWith (`${key}.`)))