dzql 0.6.36 → 0.7.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/docs/README.md +1 -1
- package/docs/for_ai.md +39 -9
- package/package.json +2 -2
- package/src/cli/codegen/notification.ts +3 -3
- package/src/cli/codegen/sql/core.ts +21 -5
- package/src/cli/codegen/sql/delete.ts +5 -7
- package/src/cli/codegen/sql/get.ts +9 -11
- package/src/cli/codegen/sql/index.ts +7 -4
- package/src/cli/codegen/sql/lookup.ts +4 -5
- package/src/cli/codegen/sql/save.ts +58 -39
- package/src/cli/codegen/sql/search.ts +44 -10
- package/src/cli/codegen/sql/utils.ts +16 -1
- package/src/cli/codegen/subscribable_sql.ts +116 -124
- package/src/cli/compile.ts +263 -0
- package/src/cli/compiler/path-validator.ts +115 -83
- package/src/cli/compiler/paths.ts +299 -379
- package/src/cli/compiler/permissions.ts +26 -26
- package/src/cli/index.ts +17 -219
- package/src/client/ws.ts +20 -10
- package/src/runtime/auth.ts +33 -2
- package/src/runtime/index.ts +47 -6
- package/src/runtime/manifest_loader.ts +10 -0
- package/src/runtime/projection.ts +19 -0
|
@@ -1,118 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path Validator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Checks every permission, notification and canSubscribe path in a domain
|
|
5
|
+
* against the domain itself: the syntax parses, every table is an entity,
|
|
6
|
+
* every column exists, and values compared with ids are integer keys.
|
|
7
|
+
* compileDomain refuses to build a domain with any error here, and the CLI
|
|
8
|
+
* `validate-paths` command reports them path by path.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import type { DomainConfig
|
|
9
|
-
import {
|
|
11
|
+
import type { DomainConfig } from "../../shared/ir.js";
|
|
12
|
+
import { generateIR } from "./ir.js";
|
|
13
|
+
import { parsePath, isIntegerType } from "./paths.js";
|
|
10
14
|
|
|
11
15
|
export interface PathValidationResult {
|
|
12
16
|
entity: string;
|
|
13
|
-
pathType: 'permissions.view' | 'permissions.create' | 'permissions.update' | 'permissions.delete' | 'notifications';
|
|
17
|
+
pathType: 'permissions.view' | 'permissions.create' | 'permissions.update' | 'permissions.delete' | 'notifications' | 'canSubscribe';
|
|
14
18
|
pathIndex: number;
|
|
15
19
|
path: string;
|
|
16
20
|
valid: boolean;
|
|
17
21
|
error?: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
interface EntityLike {
|
|
25
|
+
columns: Array<{ name: string; type: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PathKind = 'permission' | 'notification' | 'subscribe-list';
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
|
-
*
|
|
31
|
+
* Check one path against the domain. Returns the problems found (empty if none).
|
|
32
|
+
*
|
|
33
|
+
* @param entity - the entity whose row the path starts from
|
|
34
|
+
* @param fieldMap - renames start fields (a subscribable's key param is the root's id)
|
|
22
35
|
*/
|
|
23
|
-
export function
|
|
24
|
-
|
|
36
|
+
export function checkPath(
|
|
37
|
+
rule: string,
|
|
38
|
+
entityName: string,
|
|
39
|
+
entities: Record<string, EntityLike>,
|
|
40
|
+
kind: PathKind = 'permission',
|
|
41
|
+
fieldMap: Record<string, string> = {}
|
|
42
|
+
): string[] {
|
|
43
|
+
const parsed = parsePath(rule);
|
|
44
|
+
if (!parsed.isValid) return [parsed.error ?? 'invalid path'];
|
|
45
|
+
|
|
46
|
+
if (parsed.literal) {
|
|
47
|
+
return kind === 'notification' ? [`a notification path must lead to users, not ${parsed.literal}`] : [];
|
|
48
|
+
}
|
|
49
|
+
if (kind === 'subscribe-list') {
|
|
50
|
+
return ['a list subscribable has no root row to follow a path from; use TRUE, FALSE or AUTHENTICATED'];
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (const permType of ['view', 'create', 'update', 'delete'] as const) {
|
|
31
|
-
const paths = permissions[permType] || [];
|
|
32
|
-
for (let i = 0; i < paths.length; i++) {
|
|
33
|
-
const path = paths[i];
|
|
34
|
-
const result = validateSinglePath(path, entityName, domain);
|
|
35
|
-
results.push({
|
|
36
|
-
entity: entityName,
|
|
37
|
-
pathType: `permissions.${permType}`,
|
|
38
|
-
pathIndex: i,
|
|
39
|
-
path,
|
|
40
|
-
valid: result.valid,
|
|
41
|
-
error: result.error
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
53
|
+
const problems: string[] = [];
|
|
54
|
+
const columnOf = (table: string, column: string) =>
|
|
55
|
+
entities[table]?.columns.find(c => c.name === column);
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const result = validateSinglePath(path, entityName, domain);
|
|
53
|
-
results.push({
|
|
54
|
-
entity: entityName,
|
|
55
|
-
pathType: 'notifications',
|
|
56
|
-
pathIndex: i,
|
|
57
|
-
path,
|
|
58
|
-
valid: result.valid,
|
|
59
|
-
error: result.error ? `${notifyName}: ${result.error}` : undefined
|
|
60
|
-
});
|
|
61
|
-
}
|
|
57
|
+
const requireColumn = (table: string, column: string, integer: boolean) => {
|
|
58
|
+
const col = columnOf(table, column);
|
|
59
|
+
if (!col) {
|
|
60
|
+
problems.push(`no column '${column}' on ${table}`);
|
|
61
|
+
} else if (integer && !isIntegerType(col.type)) {
|
|
62
|
+
problems.push(`'${column}' is ${col.type.split(/\s/)[0]}, not an integer key`);
|
|
62
63
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
path,
|
|
76
|
-
valid: result.valid,
|
|
77
|
-
error: result.error
|
|
78
|
-
});
|
|
64
|
+
};
|
|
65
|
+
// A field of the row being checked (its start field, or an @field in a filter)
|
|
66
|
+
const requireOwnField = (field: string) => requireColumn(entityName, fieldMap[field] ?? field, true);
|
|
67
|
+
|
|
68
|
+
if (!parsed.tableFirst) requireOwnField(parsed.startField!);
|
|
69
|
+
|
|
70
|
+
const steps = parsed.tableFirst ? parsed.hops : parsed.hops.slice(1);
|
|
71
|
+
for (const hop of steps) {
|
|
72
|
+
const table = hop.table!;
|
|
73
|
+
if (!entities[table]) {
|
|
74
|
+
problems.push(`unknown table '${table}'`);
|
|
75
|
+
continue;
|
|
79
76
|
}
|
|
77
|
+
// A hop without [filters] joins the previous value on its id.
|
|
78
|
+
if (!hop.filters?.length) requireColumn(table, 'id', true);
|
|
79
|
+
for (const f of hop.filters ?? []) {
|
|
80
|
+
requireColumn(table, f.field, f.value === '$');
|
|
81
|
+
if (f.value.startsWith('@')) requireOwnField(f.value.slice(1));
|
|
82
|
+
}
|
|
83
|
+
for (const c of hop.conditions ?? []) requireColumn(table, c.field, false);
|
|
84
|
+
// The value passed on joins the next hop's key; the last one is compared with the user.
|
|
85
|
+
requireColumn(table, hop.outputField!, true);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
return
|
|
88
|
+
return problems;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/**
|
|
86
|
-
* Validate a
|
|
92
|
+
* Validate every path in a compiled domain. Each error names where the path is
|
|
93
|
+
* written, what is wrong, and the path itself.
|
|
87
94
|
*/
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
domain: DomainConfig
|
|
92
|
-
): { valid: boolean; error?: string } {
|
|
93
|
-
// Parse the path
|
|
94
|
-
const parsed = parsePath(path);
|
|
95
|
-
|
|
96
|
-
if (!parsed.isValid) {
|
|
97
|
-
return { valid: false, error: parsed.error };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Optionally validate that referenced tables exist
|
|
101
|
-
const errors: string[] = [];
|
|
95
|
+
export function validateDomainPaths(ir: { entities: Record<string, any>; subscribables: Record<string, any> }): string[] {
|
|
96
|
+
return collectResults(ir).filter(r => !r.valid).map(r => r.error!);
|
|
97
|
+
}
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
function collectResults(ir: { entities: Record<string, any>; subscribables: Record<string, any> }): PathValidationResult[] {
|
|
100
|
+
const results: PathValidationResult[] = [];
|
|
101
|
+
const record = (entity: string, pathType: PathValidationResult['pathType'], where: string, pathIndex: number, path: string, problems: string[]) => {
|
|
102
|
+
results.push({
|
|
103
|
+
entity,
|
|
104
|
+
pathType,
|
|
105
|
+
pathIndex,
|
|
106
|
+
path,
|
|
107
|
+
valid: problems.length === 0,
|
|
108
|
+
error: problems.length ? `${where}: ${problems.join('; ')} — in '${path}'` : undefined,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (const [entityName, entity] of Object.entries<any>(ir.entities)) {
|
|
113
|
+
for (const action of ['view', 'create', 'update', 'delete'] as const) {
|
|
114
|
+
(entity.permissions?.[action] ?? []).forEach((path: string, i: number) => {
|
|
115
|
+
record(entityName, `permissions.${action}`, `${entityName}.permissions.${action}`, i, path,
|
|
116
|
+
checkPath(path, entityName, ir.entities));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const [notifyName, paths] of Object.entries<any>(entity.notifications ?? {})) {
|
|
120
|
+
if (!Array.isArray(paths)) continue;
|
|
121
|
+
paths.forEach((path: string, i: number) => {
|
|
122
|
+
record(entityName, 'notifications', `${entityName}.notifications.${notifyName}`, i, path,
|
|
123
|
+
checkPath(path, entityName, ir.entities, 'notification'));
|
|
124
|
+
});
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
for (const [subName, sub] of Object.entries<any>(ir.subscribables)) {
|
|
129
|
+
const rootKey: string = sub.root?.key ?? '';
|
|
130
|
+
const isList = !rootKey;
|
|
131
|
+
const fieldMap = rootKey && !rootKey.startsWith('@') ? { [rootKey]: 'id' } : {};
|
|
132
|
+
(sub.canSubscribe ?? []).forEach((path: string, i: number) => {
|
|
133
|
+
const problems = !isList && !ir.entities[sub.root?.entity]
|
|
134
|
+
? [`unknown root entity '${sub.root?.entity}'`]
|
|
135
|
+
: checkPath(path, sub.root.entity, ir.entities, isList ? 'subscribe-list' : 'permission', fieldMap);
|
|
136
|
+
record(subName, 'canSubscribe', `${subName}.canSubscribe`, i, path, problems);
|
|
137
|
+
});
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
return
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate all paths in a domain configuration (for the CLI `validate-paths` command)
|
|
145
|
+
*/
|
|
146
|
+
export function validatePaths(domain: DomainConfig): PathValidationResult[] {
|
|
147
|
+
return collectResults(generateIR(domain));
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
/**
|