@dinoxx/dinox-cli 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/notes/actions.js +72 -20
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +12 -5
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +8 -2
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/shared.d.ts +2 -0
- package/dist/commands/notes/repo/write.d.ts +3 -0
- package/dist/commands/notes/repo/write.js +16 -5
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +29 -5
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1538 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1117 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +2 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +312 -0
- package/dist/vendor/electron/shared/note-properties.js +982 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1710 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +6 -3
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- package/dist/commands/savedViews/semantics.js +0 -738
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PropertyType, SavedViewAnalyticsInput, SavedViewAnalyticsRow, SavedViewChart, SavedViewChartMeasure, SavedViewComputedColumn, SavedViewStat, SavedViewSummaryItem } from "../shared/index.js";
|
|
2
|
+
import type { SqlExecutor } from "../rpc.js";
|
|
3
|
+
import type { SavedViewQueryScope } from "./saved-views.js";
|
|
4
|
+
/**
|
|
5
|
+
* Saved-view analytics compiler: stats / charts / summary row on top of the view scope.
|
|
6
|
+
*
|
|
7
|
+
* Contract shared with dinox-app (`database/sql/savedViewChartSql.ts`):
|
|
8
|
+
* - Same data range as the table: owner + not deleted + view filter (never the page).
|
|
9
|
+
* - Charts emit `{ bucket, value }`; stats and summary emit scalar columns `v_<i>`.
|
|
10
|
+
* - Date buckets use SQLite 'localtime'; week = ISO week starting Monday.
|
|
11
|
+
* - Multi-value dimensions (tags / multi_select) expand one note into many buckets, so only
|
|
12
|
+
* `count` is allowed there and it means "occurrences".
|
|
13
|
+
* - An item that cannot compile (deleted property, type drift) degrades to `unavailable`
|
|
14
|
+
* without failing its siblings.
|
|
15
|
+
*
|
|
16
|
+
* Desktop difference: date-only property values ("YYYY-MM-DD") are bucketed as-is instead of
|
|
17
|
+
* being shifted through 'localtime', which would move them a day back west of UTC.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_SAVED_VIEW_CHART_TIME_LIMIT = 366;
|
|
20
|
+
export declare const DEFAULT_SAVED_VIEW_CHART_CATEGORY_LIMIT = 50;
|
|
21
|
+
export type SavedViewAnalyticsPropertyTypes = ReadonlyMap<string, PropertyType>;
|
|
22
|
+
/** Compile a measure to an aggregate expression; `aliasPrefix` isolates correlated subqueries. */
|
|
23
|
+
export declare function compileSavedViewMeasure(measure: SavedViewChartMeasure, types: SavedViewAnalyticsPropertyTypes, aliasPrefix: string, options?: {
|
|
24
|
+
dimensionIsMultiValue?: boolean;
|
|
25
|
+
}): string | null;
|
|
26
|
+
export interface CompiledSavedViewSql {
|
|
27
|
+
sql: string;
|
|
28
|
+
params: unknown[];
|
|
29
|
+
}
|
|
30
|
+
export declare function compileSavedViewChartSql(chart: SavedViewChart, scope: SavedViewQueryScope, userId: string, types: SavedViewAnalyticsPropertyTypes): CompiledSavedViewSql | null;
|
|
31
|
+
/** Several scalar measures in one pass: `SELECT agg AS v_0, agg AS v_1 ...`. */
|
|
32
|
+
export declare function compileSavedViewScalarSql(measures: ReadonlyArray<SavedViewChartMeasure | null>, scope: SavedViewQueryScope, userId: string, types: SavedViewAnalyticsPropertyTypes, aliasPrefix: string): {
|
|
33
|
+
sql: string | null;
|
|
34
|
+
params: unknown[];
|
|
35
|
+
columns: Array<string | null>;
|
|
36
|
+
};
|
|
37
|
+
/** Summary cells reuse the measure IR: count = rows, sum/avg = number prop or calc column. */
|
|
38
|
+
export declare function summaryItemToMeasure(item: SavedViewSummaryItem, computedColumns: readonly SavedViewComputedColumn[], types: SavedViewAnalyticsPropertyTypes): SavedViewChartMeasure | null;
|
|
39
|
+
export declare function collectSavedViewAnalyticsPropertyKeys(parts: {
|
|
40
|
+
stats: readonly SavedViewStat[];
|
|
41
|
+
charts: readonly SavedViewChart[];
|
|
42
|
+
computedColumns: readonly SavedViewComputedColumn[];
|
|
43
|
+
summary: readonly SavedViewSummaryItem[];
|
|
44
|
+
}): string[];
|
|
45
|
+
export declare function fetchSavedViewAnalytics(executor: SqlExecutor, input: SavedViewAnalyticsInput, scope: SavedViewQueryScope): Promise<SavedViewAnalyticsRow>;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { canGroupSavedViewChartByField, canUseSavedViewChartMeasureField, isPropertyType, isSavedViewDateField, isSavedViewDisplayOnlyField, isSavedViewMultiValueField, MAX_SAVED_VIEW_CHART_LIMIT, } from "../shared/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Saved-view analytics compiler: stats / charts / summary row on top of the view scope.
|
|
4
|
+
*
|
|
5
|
+
* Contract shared with dinox-app (`database/sql/savedViewChartSql.ts`):
|
|
6
|
+
* - Same data range as the table: owner + not deleted + view filter (never the page).
|
|
7
|
+
* - Charts emit `{ bucket, value }`; stats and summary emit scalar columns `v_<i>`.
|
|
8
|
+
* - Date buckets use SQLite 'localtime'; week = ISO week starting Monday.
|
|
9
|
+
* - Multi-value dimensions (tags / multi_select) expand one note into many buckets, so only
|
|
10
|
+
* `count` is allowed there and it means "occurrences".
|
|
11
|
+
* - An item that cannot compile (deleted property, type drift) degrades to `unavailable`
|
|
12
|
+
* without failing its siblings.
|
|
13
|
+
*
|
|
14
|
+
* Desktop difference: date-only property values ("YYYY-MM-DD") are bucketed as-is instead of
|
|
15
|
+
* being shifted through 'localtime', which would move them a day back west of UTC.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_SAVED_VIEW_CHART_TIME_LIMIT = 366;
|
|
18
|
+
export const DEFAULT_SAVED_VIEW_CHART_CATEGORY_LIMIT = 50;
|
|
19
|
+
const NOTE_PROPERTIES_JSON = "CASE WHEN json_valid(n.properties) THEN n.properties ELSE '{}' END";
|
|
20
|
+
const DIMENSION_ALIAS = "chart_dim";
|
|
21
|
+
const SCALAR_VALUE_TYPES = "('text', 'integer', 'real', 'true', 'false')";
|
|
22
|
+
const SYS_DATE_COLUMNS = {
|
|
23
|
+
"sys.created_at": "n.created_at",
|
|
24
|
+
"sys.updated_at": "n.updated_at",
|
|
25
|
+
"sys.starred_at": "n.starred_at",
|
|
26
|
+
};
|
|
27
|
+
const SYS_SCALAR_COLUMNS = {
|
|
28
|
+
"sys.source": "n.source",
|
|
29
|
+
"sys.type": "n.type",
|
|
30
|
+
};
|
|
31
|
+
const SYS_ARRAY_COLUMNS = {
|
|
32
|
+
"sys.tags": "n.tags",
|
|
33
|
+
"sys.zettel_boxes": "n.zettel_boxes",
|
|
34
|
+
};
|
|
35
|
+
function quote(value) {
|
|
36
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
37
|
+
}
|
|
38
|
+
function propertyKey(field) {
|
|
39
|
+
return field.startsWith("prop.") ? field.slice("prop.".length) : null;
|
|
40
|
+
}
|
|
41
|
+
function propertyType(field, types) {
|
|
42
|
+
const key = propertyKey(field);
|
|
43
|
+
return key ? types.get(key) : undefined;
|
|
44
|
+
}
|
|
45
|
+
function safeJsonArray(column) {
|
|
46
|
+
return `CASE WHEN json_valid(${column}) AND json_type(${column}) = 'array' THEN ${column} ELSE json('[]') END`;
|
|
47
|
+
}
|
|
48
|
+
/** Invalid or missing numbers stay NULL; a bare CAST would turn bad text into zero. */
|
|
49
|
+
function numberPropertySql(key, alias) {
|
|
50
|
+
return `(SELECT CAST(${alias}.value AS REAL) FROM json_each(${NOTE_PROPERTIES_JSON}) AS ${alias} WHERE ${alias}.key = ${quote(key)} AND ${alias}.type IN ('integer', 'real'))`;
|
|
51
|
+
}
|
|
52
|
+
function timeBucketSql(base, bucket) {
|
|
53
|
+
switch (bucket) {
|
|
54
|
+
case "hour":
|
|
55
|
+
return `strftime('%Y-%m-%d %H:00', ${base})`;
|
|
56
|
+
case "day":
|
|
57
|
+
return `DATE(${base})`;
|
|
58
|
+
case "week":
|
|
59
|
+
// Step back 6 days, then forward to the nearest Monday: the ISO week start.
|
|
60
|
+
return `DATE(${base}, '-6 days', 'weekday 1')`;
|
|
61
|
+
case "month":
|
|
62
|
+
return `strftime('%Y-%m', ${base})`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function compileDimension(chart, types) {
|
|
66
|
+
const dimension = chart.dimensions[0];
|
|
67
|
+
if (!dimension)
|
|
68
|
+
return null;
|
|
69
|
+
const type = propertyType(dimension.field, types);
|
|
70
|
+
if (!canGroupSavedViewChartByField(dimension.field, type))
|
|
71
|
+
return null;
|
|
72
|
+
const isDate = isSavedViewDateField(dimension.field, type);
|
|
73
|
+
if (isDate !== Boolean(dimension.timeBucket))
|
|
74
|
+
return null;
|
|
75
|
+
if (dimension.field.startsWith("sys.")) {
|
|
76
|
+
const dateColumn = SYS_DATE_COLUMNS[dimension.field];
|
|
77
|
+
if (dateColumn && dimension.timeBucket) {
|
|
78
|
+
return {
|
|
79
|
+
expression: timeBucketSql(`datetime(${dateColumn}, 'localtime')`, dimension.timeBucket),
|
|
80
|
+
joins: "",
|
|
81
|
+
conditions: [`${dateColumn} IS NOT NULL AND ${dateColumn} <> ''`],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const scalarColumn = SYS_SCALAR_COLUMNS[dimension.field];
|
|
85
|
+
if (scalarColumn) {
|
|
86
|
+
return {
|
|
87
|
+
expression: scalarColumn,
|
|
88
|
+
joins: "",
|
|
89
|
+
conditions: [`${scalarColumn} IS NOT NULL AND ${scalarColumn} <> ''`],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const arrayColumn = SYS_ARRAY_COLUMNS[dimension.field];
|
|
93
|
+
if (arrayColumn) {
|
|
94
|
+
return {
|
|
95
|
+
expression: `${DIMENSION_ALIAS}.value`,
|
|
96
|
+
joins: `, json_each(${safeJsonArray(arrayColumn)}) AS ${DIMENSION_ALIAS}`,
|
|
97
|
+
conditions: [
|
|
98
|
+
`${DIMENSION_ALIAS}.type = 'text'`,
|
|
99
|
+
`trim(${DIMENSION_ALIAS}.value) <> ''`,
|
|
100
|
+
],
|
|
101
|
+
label: dimension.field === "sys.zettel_boxes"
|
|
102
|
+
? (bucket) => `(SELECT COALESCE(NULLIF(trim(box.path), ''), NULLIF(trim(box.name), '')) FROM c_zettel_box AS box WHERE box.id = ${bucket} AND (box.is_del IS NULL OR box.is_del = 0) LIMIT 1)`
|
|
103
|
+
: undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const key = propertyKey(dimension.field);
|
|
109
|
+
if (!key || !type)
|
|
110
|
+
return null;
|
|
111
|
+
const property = `${DIMENSION_ALIAS}_property`;
|
|
112
|
+
const value = `${DIMENSION_ALIAS}.value`;
|
|
113
|
+
const joins = ` JOIN json_each(${NOTE_PROPERTIES_JSON}) AS ${property}` +
|
|
114
|
+
` JOIN json_each(CASE WHEN ${property}.type = 'array' THEN ${property}.value ELSE json_array(${property}.value) END) AS ${DIMENSION_ALIAS}` +
|
|
115
|
+
` ON ${property}.key = ${quote(key)}` +
|
|
116
|
+
` AND ${property}.type IN ('text', 'integer', 'real', 'true', 'false', 'array')` +
|
|
117
|
+
` AND ${DIMENSION_ALIAS}.type IN ${SCALAR_VALUE_TYPES}` +
|
|
118
|
+
` AND ${value} <> ''`;
|
|
119
|
+
if (type === "date" && dimension.timeBucket) {
|
|
120
|
+
return {
|
|
121
|
+
expression: timeBucketSql(`CASE WHEN length(${value}) = 10 THEN ${value} ELSE datetime(${value}, 'localtime') END`, dimension.timeBucket),
|
|
122
|
+
joins,
|
|
123
|
+
conditions: [`${DIMENSION_ALIAS}.type = 'text'`],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const conditions = type === "number"
|
|
127
|
+
? [`${DIMENSION_ALIAS}.type IN ('integer', 'real')`]
|
|
128
|
+
: type === "checkbox"
|
|
129
|
+
? [`${DIMENSION_ALIAS}.type IN ('integer', 'real', 'true', 'false')`]
|
|
130
|
+
: [`${DIMENSION_ALIAS}.type = 'text'`];
|
|
131
|
+
return { expression: value, joins, conditions };
|
|
132
|
+
}
|
|
133
|
+
/** Compile a measure to an aggregate expression; `aliasPrefix` isolates correlated subqueries. */
|
|
134
|
+
export function compileSavedViewMeasure(measure, types, aliasPrefix, options = {}) {
|
|
135
|
+
if (measure.aggregation === "count") {
|
|
136
|
+
return measure.field === null && measure.expr === undefined ? "COUNT(*)" : null;
|
|
137
|
+
}
|
|
138
|
+
// Expanded rows != notes, so sum/avg over a multi-value dimension would double count.
|
|
139
|
+
if (options.dimensionIsMultiValue)
|
|
140
|
+
return null;
|
|
141
|
+
const aggregate = measure.aggregation === "sum" ? "SUM" : "AVG";
|
|
142
|
+
const numberSql = (field, alias) => {
|
|
143
|
+
const key = propertyKey(field);
|
|
144
|
+
if (!key || !canUseSavedViewChartMeasureField(field, types.get(key))) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return numberPropertySql(key, alias);
|
|
148
|
+
};
|
|
149
|
+
if (measure.expr) {
|
|
150
|
+
if (measure.field !== null)
|
|
151
|
+
return null;
|
|
152
|
+
const left = numberSql(measure.expr.left, `${aliasPrefix}_l`);
|
|
153
|
+
const right = numberSql(measure.expr.right, `${aliasPrefix}_r`);
|
|
154
|
+
if (!left || !right)
|
|
155
|
+
return null;
|
|
156
|
+
// Division by zero yields NULL so SUM/AVG skip the row instead of producing Inf.
|
|
157
|
+
const row = measure.expr.op === "/"
|
|
158
|
+
? `(${left} / NULLIF(${right}, 0))`
|
|
159
|
+
: `(${left} ${measure.expr.op} ${right})`;
|
|
160
|
+
return `${aggregate}(${row})`;
|
|
161
|
+
}
|
|
162
|
+
if (measure.field === null)
|
|
163
|
+
return null;
|
|
164
|
+
const single = numberSql(measure.field, aliasPrefix);
|
|
165
|
+
return single ? `${aggregate}(${single})` : null;
|
|
166
|
+
}
|
|
167
|
+
function resolveOrderClause(chart) {
|
|
168
|
+
const hasTimeBucket = Boolean(chart.dimensions[0]?.timeBucket);
|
|
169
|
+
const orderBy = chart.orderBy ?? {
|
|
170
|
+
by: hasTimeBucket ? "bucket" : "value",
|
|
171
|
+
direction: hasTimeBucket ? "asc" : "desc",
|
|
172
|
+
};
|
|
173
|
+
const direction = orderBy.direction === "asc" ? "ASC" : "DESC";
|
|
174
|
+
return orderBy.by === "bucket"
|
|
175
|
+
? `bucket ${direction}`
|
|
176
|
+
: `value ${direction}, bucket ASC`;
|
|
177
|
+
}
|
|
178
|
+
function resolveLimit(chart) {
|
|
179
|
+
const fallback = chart.dimensions[0]?.timeBucket
|
|
180
|
+
? DEFAULT_SAVED_VIEW_CHART_TIME_LIMIT
|
|
181
|
+
: DEFAULT_SAVED_VIEW_CHART_CATEGORY_LIMIT;
|
|
182
|
+
const requested = chart.limit;
|
|
183
|
+
if (typeof requested !== "number" || !Number.isInteger(requested) || requested < 1) {
|
|
184
|
+
return fallback;
|
|
185
|
+
}
|
|
186
|
+
return Math.min(requested, MAX_SAVED_VIEW_CHART_LIMIT);
|
|
187
|
+
}
|
|
188
|
+
function scopeWhere(scope, userId) {
|
|
189
|
+
return {
|
|
190
|
+
sql: `n.user_no = ? AND (n.is_del IS NULL OR n.is_del = 0) AND (${scope.where.sql})`,
|
|
191
|
+
params: [userId, ...scope.where.params],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function compileSavedViewChartSql(chart, scope, userId, types) {
|
|
195
|
+
const dimension = compileDimension(chart, types);
|
|
196
|
+
const measure = chart.measures[0];
|
|
197
|
+
if (!dimension || !measure)
|
|
198
|
+
return null;
|
|
199
|
+
const dimensionField = chart.dimensions[0].field;
|
|
200
|
+
const aggregate = compileSavedViewMeasure(measure, types, "chart_measure", {
|
|
201
|
+
dimensionIsMultiValue: isSavedViewMultiValueField(dimensionField, propertyType(dimensionField, types)),
|
|
202
|
+
});
|
|
203
|
+
if (!aggregate)
|
|
204
|
+
return null;
|
|
205
|
+
const where = scopeWhere(scope, userId);
|
|
206
|
+
const inner = `SELECT ${dimension.expression} AS bucket, ${aggregate} AS value ` +
|
|
207
|
+
`FROM c_note AS n${dimension.joins} ` +
|
|
208
|
+
`WHERE ${[where.sql, ...dimension.conditions].join(" AND ")} ` +
|
|
209
|
+
"GROUP BY bucket HAVING bucket IS NOT NULL " +
|
|
210
|
+
`ORDER BY ${resolveOrderClause(chart)} LIMIT ${resolveLimit(chart)}`;
|
|
211
|
+
const sql = dimension.label
|
|
212
|
+
? `SELECT grouped.bucket, grouped.value, ${dimension.label("grouped.bucket")} AS label FROM (${inner}) AS grouped`
|
|
213
|
+
: inner;
|
|
214
|
+
return { sql, params: where.params };
|
|
215
|
+
}
|
|
216
|
+
/** Several scalar measures in one pass: `SELECT agg AS v_0, agg AS v_1 ...`. */
|
|
217
|
+
export function compileSavedViewScalarSql(measures, scope, userId, types, aliasPrefix) {
|
|
218
|
+
const columns = measures.map((measure, index) => measure ? compileSavedViewMeasure(measure, types, `${aliasPrefix}_${index}`) : null);
|
|
219
|
+
const selected = columns.flatMap((expression, index) => expression ? [`${expression} AS v_${index}`] : []);
|
|
220
|
+
if (selected.length === 0)
|
|
221
|
+
return { sql: null, params: [], columns };
|
|
222
|
+
const where = scopeWhere(scope, userId);
|
|
223
|
+
return {
|
|
224
|
+
sql: `SELECT ${selected.join(", ")} FROM c_note AS n WHERE ${where.sql}`,
|
|
225
|
+
params: where.params,
|
|
226
|
+
columns,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Summary cells reuse the measure IR: count = rows, sum/avg = number prop or calc column. */
|
|
230
|
+
export function summaryItemToMeasure(item, computedColumns, types) {
|
|
231
|
+
if (item.column.startsWith("sys.") &&
|
|
232
|
+
isSavedViewDisplayOnlyField(item.column)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (item.aggregation === "count")
|
|
236
|
+
return { field: null, aggregation: "count" };
|
|
237
|
+
if (item.column.startsWith("calc.")) {
|
|
238
|
+
const id = item.column.slice("calc.".length);
|
|
239
|
+
const computed = computedColumns.find((column) => column.id === id);
|
|
240
|
+
return computed
|
|
241
|
+
? { field: null, aggregation: item.aggregation, expr: computed.expr }
|
|
242
|
+
: null;
|
|
243
|
+
}
|
|
244
|
+
const field = item.column;
|
|
245
|
+
return canUseSavedViewChartMeasureField(field, propertyType(field, types))
|
|
246
|
+
? { field, aggregation: item.aggregation }
|
|
247
|
+
: null;
|
|
248
|
+
}
|
|
249
|
+
export function collectSavedViewAnalyticsPropertyKeys(parts) {
|
|
250
|
+
const keys = new Set();
|
|
251
|
+
const addField = (field) => {
|
|
252
|
+
const key = field ? propertyKey(field) : null;
|
|
253
|
+
if (key)
|
|
254
|
+
keys.add(key);
|
|
255
|
+
};
|
|
256
|
+
const addMeasure = (measure) => {
|
|
257
|
+
addField(measure?.field);
|
|
258
|
+
addField(measure?.expr?.left);
|
|
259
|
+
addField(measure?.expr?.right);
|
|
260
|
+
};
|
|
261
|
+
for (const stat of parts.stats)
|
|
262
|
+
addMeasure(stat.measure);
|
|
263
|
+
for (const chart of parts.charts) {
|
|
264
|
+
addField(chart.dimensions[0]?.field);
|
|
265
|
+
addMeasure(chart.measures[0]);
|
|
266
|
+
}
|
|
267
|
+
for (const column of parts.computedColumns) {
|
|
268
|
+
addField(column.expr.left);
|
|
269
|
+
addField(column.expr.right);
|
|
270
|
+
}
|
|
271
|
+
for (const item of parts.summary) {
|
|
272
|
+
if (item.column.startsWith("prop."))
|
|
273
|
+
addField(item.column);
|
|
274
|
+
}
|
|
275
|
+
return [...keys];
|
|
276
|
+
}
|
|
277
|
+
async function loadPropertyTypes(executor, userId, keys) {
|
|
278
|
+
const types = new Map();
|
|
279
|
+
if (keys.length === 0)
|
|
280
|
+
return types;
|
|
281
|
+
const rows = await executor.getAll(`
|
|
282
|
+
SELECT ranked.key, ranked.type
|
|
283
|
+
FROM (
|
|
284
|
+
SELECT
|
|
285
|
+
key,
|
|
286
|
+
type,
|
|
287
|
+
ROW_NUMBER() OVER (
|
|
288
|
+
PARTITION BY key
|
|
289
|
+
ORDER BY
|
|
290
|
+
COALESCE(NULLIF(updated_at, ''), NULLIF(created_at, ''), id) DESC,
|
|
291
|
+
id DESC
|
|
292
|
+
) AS key_rank
|
|
293
|
+
FROM c_property_def
|
|
294
|
+
WHERE user_id = ?
|
|
295
|
+
AND key IN (${keys.map(() => "?").join(", ")})
|
|
296
|
+
AND (is_del IS NULL OR is_del = 0)
|
|
297
|
+
) AS ranked
|
|
298
|
+
WHERE ranked.key_rank = 1
|
|
299
|
+
`, [userId, ...keys]);
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
if (row.key && isPropertyType(row.type))
|
|
302
|
+
types.set(row.key, row.type);
|
|
303
|
+
}
|
|
304
|
+
return types;
|
|
305
|
+
}
|
|
306
|
+
function toFiniteNumber(raw) {
|
|
307
|
+
if (typeof raw === "number")
|
|
308
|
+
return Number.isFinite(raw) ? raw : null;
|
|
309
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
310
|
+
const parsed = Number(raw);
|
|
311
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
export async function fetchSavedViewAnalytics(executor, input, scope) {
|
|
316
|
+
const stats = input.draft?.stats ?? scope.config.stats ?? [];
|
|
317
|
+
const charts = input.draft?.charts ?? scope.config.charts ?? [];
|
|
318
|
+
const computedColumns = input.draft?.computedColumns ?? scope.config.computedColumns ?? [];
|
|
319
|
+
const summary = input.draft?.summary ?? scope.config.summary ?? [];
|
|
320
|
+
const types = await loadPropertyTypes(executor, input.userId, collectSavedViewAnalyticsPropertyKeys({ stats, charts, computedColumns, summary }));
|
|
321
|
+
const readScalars = async (measures, aliasPrefix) => {
|
|
322
|
+
const compiled = compileSavedViewScalarSql(measures, scope, input.userId, types, aliasPrefix);
|
|
323
|
+
const row = compiled.sql
|
|
324
|
+
? await executor.getOptional(compiled.sql, compiled.params)
|
|
325
|
+
: null;
|
|
326
|
+
return compiled.columns.map((expression, index) => ({
|
|
327
|
+
unavailable: expression === null,
|
|
328
|
+
value: expression && row ? toFiniteNumber(row[`v_${index}`]) : null,
|
|
329
|
+
}));
|
|
330
|
+
};
|
|
331
|
+
const statValues = await readScalars(stats.map((stat) => stat.measure), "stat");
|
|
332
|
+
const summaryValues = await readScalars(summary.map((item) => summaryItemToMeasure(item, computedColumns, types)), "summary");
|
|
333
|
+
const chartResults = [];
|
|
334
|
+
for (const chart of charts) {
|
|
335
|
+
const compiled = compileSavedViewChartSql(chart, scope, input.userId, types);
|
|
336
|
+
if (!compiled) {
|
|
337
|
+
chartResults.push({ id: chart.id, rows: [], unavailable: true });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const rows = await executor.getAll(compiled.sql, compiled.params);
|
|
341
|
+
chartResults.push({
|
|
342
|
+
id: chart.id,
|
|
343
|
+
unavailable: false,
|
|
344
|
+
rows: rows.map((row) => ({
|
|
345
|
+
bucket: row.bucket ?? null,
|
|
346
|
+
value: toFiniteNumber(row.value),
|
|
347
|
+
...(row.label !== undefined ? { label: row.label ?? null } : {}),
|
|
348
|
+
})),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
stats: stats.map((stat, index) => ({
|
|
353
|
+
id: stat.id,
|
|
354
|
+
value: statValues[index]?.value ?? null,
|
|
355
|
+
unavailable: statValues[index]?.unavailable ?? true,
|
|
356
|
+
})),
|
|
357
|
+
charts: chartResults,
|
|
358
|
+
summary: summary.map((item, index) => ({
|
|
359
|
+
column: item.column,
|
|
360
|
+
aggregation: item.aggregation,
|
|
361
|
+
value: summaryValues[index]?.value ?? null,
|
|
362
|
+
unavailable: summaryValues[index]?.unavailable ?? true,
|
|
363
|
+
})),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inputs: a dynamic saved-view date operator, an optional rolling-day count, current time, and IANA timezone.
|
|
3
|
+
* Outputs: deterministic half-open calendar boundaries for main-owned saved-view SQL compilation.
|
|
4
|
+
*/
|
|
5
|
+
import { type SavedViewFilterOperator } from "../shared/index.js";
|
|
6
|
+
type DynamicDateOperator = Extract<SavedViewFilterOperator, "withinLastDays" | "isToday" | "isThisWeek" | "isThisMonth">;
|
|
7
|
+
export type SavedViewRelativeDateRange = {
|
|
8
|
+
timeZone: string;
|
|
9
|
+
startDate: string;
|
|
10
|
+
endDateExclusive: string;
|
|
11
|
+
startInstant: string;
|
|
12
|
+
endInstantExclusive: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function resolveSavedViewTimeZone(value?: string): string;
|
|
15
|
+
export declare function resolveSavedViewRelativeDateRange(options: {
|
|
16
|
+
operator: DynamicDateOperator;
|
|
17
|
+
value?: unknown;
|
|
18
|
+
now?: Date;
|
|
19
|
+
timeZone?: string;
|
|
20
|
+
}): SavedViewRelativeDateRange;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Generated by scripts/sync-electron-domain.ts from dinox-electron/packages/main/src/modules/powersync/typed-rpc/domain/saved-view-relative-date.ts. Do not edit manually.
|
|
2
|
+
/**
|
|
3
|
+
* Inputs: a dynamic saved-view date operator, an optional rolling-day count, current time, and IANA timezone.
|
|
4
|
+
* Outputs: deterministic half-open calendar boundaries for main-owned saved-view SQL compilation.
|
|
5
|
+
*/
|
|
6
|
+
import { MAX_SAVED_VIEW_RELATIVE_DAYS, } from "../shared/index.js";
|
|
7
|
+
const UTC_TIME_ZONE = "UTC";
|
|
8
|
+
const dateTimeFormatters = new Map();
|
|
9
|
+
export function resolveSavedViewTimeZone(value) {
|
|
10
|
+
const requested = value?.trim();
|
|
11
|
+
if (requested) {
|
|
12
|
+
if (!isSupportedTimeZone(requested)) {
|
|
13
|
+
throw new Error("SAVED_VIEW_INVALID_TIME_ZONE");
|
|
14
|
+
}
|
|
15
|
+
return requested;
|
|
16
|
+
}
|
|
17
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18
|
+
return local && isSupportedTimeZone(local) ? local : UTC_TIME_ZONE;
|
|
19
|
+
}
|
|
20
|
+
export function resolveSavedViewRelativeDateRange(options) {
|
|
21
|
+
const now = options.now ?? new Date();
|
|
22
|
+
if (Number.isNaN(now.getTime())) {
|
|
23
|
+
throw new Error("SAVED_VIEW_INVALID_CURRENT_TIME");
|
|
24
|
+
}
|
|
25
|
+
const timeZone = resolveSavedViewTimeZone(options.timeZone);
|
|
26
|
+
const today = getCalendarDate(now, timeZone);
|
|
27
|
+
let start;
|
|
28
|
+
let endExclusive;
|
|
29
|
+
switch (options.operator) {
|
|
30
|
+
case "withinLastDays": {
|
|
31
|
+
const days = options.value;
|
|
32
|
+
if (typeof days !== "number" ||
|
|
33
|
+
!Number.isInteger(days) ||
|
|
34
|
+
days < 1 ||
|
|
35
|
+
days > MAX_SAVED_VIEW_RELATIVE_DAYS) {
|
|
36
|
+
throw new Error("SAVED_VIEW_INVALID_RELATIVE_DAYS");
|
|
37
|
+
}
|
|
38
|
+
start = addCalendarDays(today, -(days - 1));
|
|
39
|
+
endExclusive = addCalendarDays(today, 1);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "isToday":
|
|
43
|
+
start = today;
|
|
44
|
+
endExclusive = addCalendarDays(today, 1);
|
|
45
|
+
break;
|
|
46
|
+
case "isThisWeek": {
|
|
47
|
+
// Saved views use ISO calendar weeks (Monday through Sunday).
|
|
48
|
+
const dayOfWeek = getIsoDayOfWeek(today);
|
|
49
|
+
start = addCalendarDays(today, -(dayOfWeek - 1));
|
|
50
|
+
endExclusive = addCalendarDays(start, 7);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "isThisMonth":
|
|
54
|
+
start = { year: today.year, month: today.month, day: 1 };
|
|
55
|
+
endExclusive = addCalendarMonths(start, 1);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
timeZone,
|
|
60
|
+
startDate: formatCalendarDate(start),
|
|
61
|
+
endDateExclusive: formatCalendarDate(endExclusive),
|
|
62
|
+
startInstant: zonedMidnightToInstant(start, timeZone).toISOString(),
|
|
63
|
+
endInstantExclusive: zonedMidnightToInstant(endExclusive, timeZone).toISOString(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function isSupportedTimeZone(value) {
|
|
67
|
+
try {
|
|
68
|
+
new Intl.DateTimeFormat("en", { timeZone: value }).format(new Date());
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getDateTimeFormatter(timeZone) {
|
|
76
|
+
const cached = dateTimeFormatters.get(timeZone);
|
|
77
|
+
if (cached)
|
|
78
|
+
return cached;
|
|
79
|
+
const formatter = new Intl.DateTimeFormat("en-US-u-ca-iso8601-nu-latn", {
|
|
80
|
+
timeZone,
|
|
81
|
+
year: "numeric",
|
|
82
|
+
month: "2-digit",
|
|
83
|
+
day: "2-digit",
|
|
84
|
+
hour: "2-digit",
|
|
85
|
+
minute: "2-digit",
|
|
86
|
+
second: "2-digit",
|
|
87
|
+
hourCycle: "h23",
|
|
88
|
+
});
|
|
89
|
+
dateTimeFormatters.set(timeZone, formatter);
|
|
90
|
+
return formatter;
|
|
91
|
+
}
|
|
92
|
+
function getZonedDateTimeParts(value, timeZone) {
|
|
93
|
+
const parts = getDateTimeFormatter(timeZone).formatToParts(value);
|
|
94
|
+
const read = (type) => {
|
|
95
|
+
const parsed = Number(parts.find((part) => part.type === type)?.value);
|
|
96
|
+
if (!Number.isFinite(parsed)) {
|
|
97
|
+
throw new Error("SAVED_VIEW_TIME_ZONE_CONVERSION_FAILED");
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
year: read("year"),
|
|
103
|
+
month: read("month"),
|
|
104
|
+
day: read("day"),
|
|
105
|
+
hour: read("hour"),
|
|
106
|
+
minute: read("minute"),
|
|
107
|
+
second: read("second"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function getCalendarDate(value, timeZone) {
|
|
111
|
+
const parts = getZonedDateTimeParts(value, timeZone);
|
|
112
|
+
return { year: parts.year, month: parts.month, day: parts.day };
|
|
113
|
+
}
|
|
114
|
+
function addCalendarDays(value, days) {
|
|
115
|
+
const date = new Date(Date.UTC(value.year, value.month - 1, value.day + days));
|
|
116
|
+
return {
|
|
117
|
+
year: date.getUTCFullYear(),
|
|
118
|
+
month: date.getUTCMonth() + 1,
|
|
119
|
+
day: date.getUTCDate(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function addCalendarMonths(value, months) {
|
|
123
|
+
const date = new Date(Date.UTC(value.year, value.month - 1 + months, 1));
|
|
124
|
+
return {
|
|
125
|
+
year: date.getUTCFullYear(),
|
|
126
|
+
month: date.getUTCMonth() + 1,
|
|
127
|
+
day: date.getUTCDate(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function getIsoDayOfWeek(value) {
|
|
131
|
+
const day = new Date(Date.UTC(value.year, value.month - 1, value.day)).getUTCDay();
|
|
132
|
+
return day === 0 ? 7 : day;
|
|
133
|
+
}
|
|
134
|
+
function formatCalendarDate(value) {
|
|
135
|
+
return [
|
|
136
|
+
String(value.year).padStart(4, "0"),
|
|
137
|
+
String(value.month).padStart(2, "0"),
|
|
138
|
+
String(value.day).padStart(2, "0"),
|
|
139
|
+
].join("-");
|
|
140
|
+
}
|
|
141
|
+
function zonedMidnightToInstant(value, timeZone) {
|
|
142
|
+
const targetAsUtc = Date.UTC(value.year, value.month - 1, value.day);
|
|
143
|
+
let instant = targetAsUtc;
|
|
144
|
+
for (let iteration = 0; iteration < 4; iteration += 1) {
|
|
145
|
+
const actual = getZonedDateTimeParts(new Date(instant), timeZone);
|
|
146
|
+
const actualAsUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second);
|
|
147
|
+
const correction = targetAsUtc - actualAsUtc;
|
|
148
|
+
instant += correction;
|
|
149
|
+
if (correction === 0)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return new Date(instant);
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PropertyOption, PropertyType, SavedViewConfig, SavedViewCountInput, SavedViewFieldRef, SavedViewFilter, SavedViewNoteDetailInput, SavedViewNoteDetailSection, SavedViewPageInput, SavedViewPageRow } from "../shared/index.js";
|
|
2
|
+
import type { AbstractPowerSyncDatabase as PowerSyncDatabase } from "@powersync/common";
|
|
3
|
+
import { type CommandDefinition, type ReadModelDefinition, type SqlExecutor } from "../rpc.js";
|
|
4
|
+
export type SavedViewResolvedDefinition = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string | null;
|
|
7
|
+
emoji: string | null;
|
|
8
|
+
filter_json: string | null;
|
|
9
|
+
view_type: string | null;
|
|
10
|
+
config_json: string | null;
|
|
11
|
+
updated_at: string | null;
|
|
12
|
+
};
|
|
13
|
+
export type SavedViewSqlFragment = {
|
|
14
|
+
sql: string;
|
|
15
|
+
params: unknown[];
|
|
16
|
+
};
|
|
17
|
+
export interface SavedViewQueryScope {
|
|
18
|
+
definition: SavedViewResolvedDefinition;
|
|
19
|
+
filter: SavedViewFilter;
|
|
20
|
+
config: SavedViewConfig;
|
|
21
|
+
semantics: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>;
|
|
22
|
+
where: SavedViewSqlFragment;
|
|
23
|
+
order: SavedViewSqlFragment;
|
|
24
|
+
}
|
|
25
|
+
type SavedViewSemanticType = "text" | "select" | "list" | "number" | "date" | "boolean";
|
|
26
|
+
export type SavedViewFieldSemantic = {
|
|
27
|
+
type: SavedViewSemanticType;
|
|
28
|
+
propertyType?: PropertyType;
|
|
29
|
+
dateMode?: "date" | "datetime";
|
|
30
|
+
options?: readonly PropertyOption[];
|
|
31
|
+
displayOnly?: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function createSavedViewReadModels(): ReadModelDefinition[];
|
|
34
|
+
export declare function createSavedViewCommands(): CommandDefinition[];
|
|
35
|
+
export declare function resolveSavedViewQueryScope(executor: SqlExecutor, input: Pick<SavedViewPageInput, "viewId" | "userId" | "timeZone">): Promise<SavedViewQueryScope>;
|
|
36
|
+
export declare function fetchSavedViewNoteDetail(executor: SqlExecutor, input: SavedViewNoteDetailInput): Promise<SavedViewNoteDetailSection[]>;
|
|
37
|
+
export declare function fetchSavedViewScopedPage(executor: SqlExecutor, input: SavedViewPageInput, scope?: SavedViewQueryScope): Promise<SavedViewPageRow[]>;
|
|
38
|
+
export declare function countSavedViewScopedRows(executor: SqlExecutor, input: SavedViewCountInput, scope?: SavedViewQueryScope): Promise<number>;
|
|
39
|
+
export declare function filterSavedViewNoteIds(executor: SqlExecutor, input: Pick<SavedViewPageInput, "viewId" | "userId" | "timeZone"> & {
|
|
40
|
+
noteIds: readonly string[];
|
|
41
|
+
}, scope?: SavedViewQueryScope): Promise<string[]>;
|
|
42
|
+
export type SavedViewFilterCompileOptions = {
|
|
43
|
+
now?: Date;
|
|
44
|
+
timeZone?: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function compileFilter(filter: SavedViewFilter, semantics?: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>, options?: SavedViewFilterCompileOptions): SavedViewSqlFragment;
|
|
47
|
+
export declare function compileOrder(config: SavedViewConfig, semantics?: ReadonlyMap<SavedViewFieldRef, SavedViewFieldSemantic>): SavedViewSqlFragment;
|
|
48
|
+
/**
|
|
49
|
+
* Read-modify-write of one view's config inside a single write transaction, for writers
|
|
50
|
+
* that only touch the analytics layer (Agent tools). `apply` receives the stored config and
|
|
51
|
+
* returns the next one; filter, columns and sort stay as stored.
|
|
52
|
+
*/
|
|
53
|
+
export declare function updateSavedViewConfig(db: PowerSyncDatabase, input: {
|
|
54
|
+
viewId: string;
|
|
55
|
+
userId: string;
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
}, apply: (config: SavedViewConfig) => SavedViewConfig): Promise<{
|
|
58
|
+
previous: SavedViewConfig;
|
|
59
|
+
next: SavedViewConfig;
|
|
60
|
+
}>;
|
|
61
|
+
export {};
|