rastack 0.0.16
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/CHANGELOG.md +43013 -0
- package/dist/.prettierrc +7 -0
- package/dist/compile/analyze.d.ts +12 -0
- package/dist/compile/analyze.js +233 -0
- package/dist/compile/index.d.ts +19 -0
- package/dist/compile/index.js +107 -0
- package/dist/compile/manifest.d.ts +4 -0
- package/dist/compile/manifest.js +12 -0
- package/dist/compile/model.d.ts +54 -0
- package/dist/compile/model.js +7 -0
- package/dist/compile/openapi.d.ts +11 -0
- package/dist/compile/openapi.js +233 -0
- package/dist/compile/program.d.ts +10 -0
- package/dist/compile/program.js +61 -0
- package/dist/csv-schema.d.ts +28 -0
- package/dist/csv-schema.js +162 -0
- package/dist/define/index.d.ts +117 -0
- package/dist/define/index.js +74 -0
- package/dist/entity-generation/delete-method.d.ts +1 -0
- package/dist/entity-generation/delete-method.js +31 -0
- package/dist/entity-generation/form.d.ts +13 -0
- package/dist/entity-generation/form.js +136 -0
- package/dist/entity-generation/get-method.d.ts +1 -0
- package/dist/entity-generation/get-method.js +67 -0
- package/dist/entity-generation/imports.d.ts +5 -0
- package/dist/entity-generation/imports.js +91 -0
- package/dist/entity-generation/sync-method.d.ts +30 -0
- package/dist/entity-generation/sync-method.js +158 -0
- package/dist/entity-generation/update-method.d.ts +1 -0
- package/dist/entity-generation/update-method.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/rad-compile.d.ts +11 -0
- package/dist/rad-compile.js +82 -0
- package/dist/rad-wasm-build.d.ts +14 -0
- package/dist/rad-wasm-build.js +74 -0
- package/dist/rad.d.ts +17 -0
- package/dist/rad.js +88 -0
- package/dist/scan.d.ts +12 -0
- package/dist/scan.js +79 -0
- package/dist/schema/camel-to-pastel.d.ts +1 -0
- package/dist/schema/camel-to-pastel.js +7 -0
- package/dist/schema/capitalise-first-letter.d.ts +1 -0
- package/dist/schema/capitalise-first-letter.js +7 -0
- package/dist/schema/extract-response.d.ts +2 -0
- package/dist/schema/extract-response.js +30 -0
- package/dist/schema/fetch-schema.d.ts +7 -0
- package/dist/schema/fetch-schema.js +23 -0
- package/dist/schema/remove-non-model-paths.d.ts +2 -0
- package/dist/schema/remove-non-model-paths.js +15 -0
- package/dist/schema/to-camel-case.d.ts +1 -0
- package/dist/schema/to-camel-case.js +11 -0
- package/dist/schema/to-pastel-case.d.ts +1 -0
- package/dist/schema/to-pastel-case.js +10 -0
- package/dist/schema-convert.d.ts +1 -0
- package/dist/schema-convert.js +63 -0
- package/dist/schema-entities.d.ts +14 -0
- package/dist/schema-entities.js +293 -0
- package/dist/schema-fetch.d.ts +1 -0
- package/dist/schema-fetch.js +81 -0
- package/dist/schema-full.d.ts +1 -0
- package/dist/schema-full.js +45 -0
- package/dist/schema-index.d.ts +1 -0
- package/dist/schema-index.js +34 -0
- package/dist/schema-params.d.ts +1 -0
- package/dist/schema-params.js +100 -0
- package/dist/seed.d.ts +17 -0
- package/dist/seed.js +252 -0
- package/hooks/form/form.ts +207 -0
- package/hooks/form/index.ts +8 -0
- package/hooks/form/interfaces.ts +217 -0
- package/hooks/form/structure.ts +39 -0
- package/hooks/form/validate-schema.ts +49 -0
- package/hooks/index.ts +3 -0
- package/hooks/query/api.ts +42 -0
- package/hooks/query/delete.ts +45 -0
- package/hooks/query/fetch.ts +48 -0
- package/hooks/query/index.ts +21 -0
- package/hooks/query/interfaces.ts +111 -0
- package/hooks/query/list.ts +286 -0
- package/hooks/query/update.ts +88 -0
- package/hooks/query/url.ts +31 -0
- package/hooks/real-time/index.ts +1 -0
- package/hooks/real-time/pusher.ts +43 -0
- package/jest.config.cjs +6 -0
- package/package.json +57 -0
- package/provider/index.ts +13 -0
- package/provider/provider.tsx +187 -0
- package/provider/types.ts +114 -0
- package/provider/warehouse.ts +167 -0
- package/provider/wasm.ts +124 -0
- package/runtime.ts +3 -0
- package/src/.prettierrc +7 -0
- package/src/compile/analyze.ts +224 -0
- package/src/compile/index.ts +86 -0
- package/src/compile/manifest.ts +10 -0
- package/src/compile/model.ts +69 -0
- package/src/compile/openapi.ts +266 -0
- package/src/compile/program.ts +40 -0
- package/src/csv-schema.ts +187 -0
- package/src/define/index.ts +162 -0
- package/src/entity-generation/delete-method.ts +37 -0
- package/src/entity-generation/form.ts +169 -0
- package/src/entity-generation/get-method.ts +80 -0
- package/src/entity-generation/imports.ts +110 -0
- package/src/entity-generation/sync-method.ts +222 -0
- package/src/entity-generation/update-method.ts +38 -0
- package/src/index.ts +77 -0
- package/src/rad-compile.ts +88 -0
- package/src/rad-wasm-build.ts +102 -0
- package/src/rad.ts +101 -0
- package/src/scan.ts +89 -0
- package/src/schema/camel-to-pastel.ts +3 -0
- package/src/schema/capitalise-first-letter.ts +3 -0
- package/src/schema/extract-response.ts +38 -0
- package/src/schema/fetch-schema.ts +12 -0
- package/src/schema/remove-non-model-paths.ts +13 -0
- package/src/schema/to-camel-case.ts +7 -0
- package/src/schema/to-pastel-case.ts +6 -0
- package/src/schema-convert.ts +61 -0
- package/src/schema-entities.ts +401 -0
- package/src/schema-fetch.ts +82 -0
- package/src/schema-full.ts +38 -0
- package/src/schema-index.ts +29 -0
- package/src/schema-params.ts +116 -0
- package/src/seed.ts +297 -0
- package/sync/engine.ts +392 -0
- package/sync/hooks.ts +450 -0
- package/sync/index.ts +8 -0
- package/sync/persistence.ts +237 -0
- package/sync/provider.tsx +91 -0
- package/sync/registry.ts +36 -0
- package/sync/store.ts +126 -0
- package/sync/transactions.ts +300 -0
- package/sync/types.ts +94 -0
- package/test/compile.spec.ts +192 -0
- package/test/csv-schema.spec.ts +143 -0
- package/test/schema-entities.spec.ts +688 -0
- package/tsconfig.json +24 -0
- package/types.ts +19 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openApiToJson = exports.buildOpenApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Emit an OpenAPI 3 document from the analysed resources. The output reproduces
|
|
6
|
+
* the *exact* contract the existing `tools/` codegen and `stack/` runtime rely
|
|
7
|
+
* on — `/api/{app}/v1/{model}/` URL segments and the `x-rastack-sync*` vendor
|
|
8
|
+
* extensions — so those packages are unchanged. Foreign keys additionally carry
|
|
9
|
+
* a new `x-rastack-relation` extension so downstream tooling (and the admin) can
|
|
10
|
+
* follow relations.
|
|
11
|
+
*/
|
|
12
|
+
function buildOpenApi(resources) {
|
|
13
|
+
var _a;
|
|
14
|
+
const paths = {};
|
|
15
|
+
const schemas = {};
|
|
16
|
+
for (const resource of resources) {
|
|
17
|
+
const P = pascal(resource.model);
|
|
18
|
+
const listUrl = `/api/${resource.app}/v1/${resource.model}/`;
|
|
19
|
+
const detailUrl = `${listUrl}{id}/`;
|
|
20
|
+
const local = ((_a = resource.sync) === null || _a === void 0 ? void 0 : _a.mode) === "local";
|
|
21
|
+
schemas[P] = readSchema(resource);
|
|
22
|
+
schemas[`Paginated${P}List`] = paginatedSchema(P);
|
|
23
|
+
schemas[`${P}Request`] = writeSchema(resource, false);
|
|
24
|
+
schemas[`Patched${P}Request`] = writeSchema(resource, true);
|
|
25
|
+
paths[listUrl] = {
|
|
26
|
+
get: syncStamp({
|
|
27
|
+
operationId: `list${P}`,
|
|
28
|
+
parameters: [
|
|
29
|
+
queryParam("page", "integer"),
|
|
30
|
+
queryParam("search", "string"),
|
|
31
|
+
],
|
|
32
|
+
responses: jsonResponse("200", `Paginated${P}List`),
|
|
33
|
+
}, resource, "list"),
|
|
34
|
+
post: syncStamp({
|
|
35
|
+
operationId: `create${P}`,
|
|
36
|
+
requestBody: jsonBody(`${P}Request`),
|
|
37
|
+
responses: jsonResponse("201", P),
|
|
38
|
+
}, resource, "create"),
|
|
39
|
+
};
|
|
40
|
+
paths[detailUrl] = {
|
|
41
|
+
get: syncStamp({
|
|
42
|
+
operationId: `get${P}ById`,
|
|
43
|
+
parameters: [idParam()],
|
|
44
|
+
responses: jsonResponse("200", P),
|
|
45
|
+
}, resource, "retrieve"),
|
|
46
|
+
put: syncStamp({
|
|
47
|
+
operationId: `update${P}`,
|
|
48
|
+
parameters: [idParam()],
|
|
49
|
+
requestBody: jsonBody(`${P}Request`),
|
|
50
|
+
responses: jsonResponse("200", P),
|
|
51
|
+
}, resource, "update"),
|
|
52
|
+
patch: syncStamp({
|
|
53
|
+
operationId: `partialUpdate${P}`,
|
|
54
|
+
parameters: [idParam()],
|
|
55
|
+
requestBody: jsonBody(`Patched${P}Request`),
|
|
56
|
+
responses: jsonResponse("200", P),
|
|
57
|
+
}, resource, "partial_update"),
|
|
58
|
+
delete: syncStamp({
|
|
59
|
+
operationId: `delete${P}`,
|
|
60
|
+
parameters: [idParam()],
|
|
61
|
+
responses: { "204": { description: "No response body" } },
|
|
62
|
+
}, resource, "destroy"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
openapi: "3.0.3",
|
|
67
|
+
info: { title: "React API Stack", version: "1.0.0" },
|
|
68
|
+
paths,
|
|
69
|
+
components: { schemas },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
exports.buildOpenApi = buildOpenApi;
|
|
73
|
+
function openApiToJson(doc) {
|
|
74
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
75
|
+
}
|
|
76
|
+
exports.openApiToJson = openApiToJson;
|
|
77
|
+
// -- schema builders --------------------------------------------------------
|
|
78
|
+
function readSchema(resource) {
|
|
79
|
+
const properties = {
|
|
80
|
+
id: idProperty(resource),
|
|
81
|
+
};
|
|
82
|
+
const required = [];
|
|
83
|
+
for (const field of resource.fields) {
|
|
84
|
+
properties[field.name] = fieldProperty(field);
|
|
85
|
+
if (!field.null && field.default === undefined)
|
|
86
|
+
required.push(field.name);
|
|
87
|
+
}
|
|
88
|
+
return { type: "object", properties, required };
|
|
89
|
+
}
|
|
90
|
+
function writeSchema(resource, patched) {
|
|
91
|
+
const properties = {};
|
|
92
|
+
const required = [];
|
|
93
|
+
for (const field of resource.fields) {
|
|
94
|
+
if (field.primaryKey)
|
|
95
|
+
continue;
|
|
96
|
+
properties[field.name] = fieldProperty(field);
|
|
97
|
+
if (!patched && !field.null && field.default === undefined) {
|
|
98
|
+
required.push(field.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const schema = { type: "object", properties };
|
|
102
|
+
if (required.length)
|
|
103
|
+
schema.required = required;
|
|
104
|
+
return schema;
|
|
105
|
+
}
|
|
106
|
+
function paginatedSchema(pascalModel) {
|
|
107
|
+
return {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
count: { type: "integer" },
|
|
111
|
+
next: { type: "string", nullable: true },
|
|
112
|
+
previous: { type: "string", nullable: true },
|
|
113
|
+
results: {
|
|
114
|
+
type: "array",
|
|
115
|
+
items: { $ref: `#/components/schemas/${pascalModel}` },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["count", "results"],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function fieldProperty(field) {
|
|
122
|
+
if (field.type === "fk") {
|
|
123
|
+
return {
|
|
124
|
+
type: "integer",
|
|
125
|
+
"x-rastack-relation": field.relation,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const base = scalarProperty(field.type);
|
|
129
|
+
if (field.maxLength !== undefined)
|
|
130
|
+
base.maxLength = field.maxLength;
|
|
131
|
+
if (field.null)
|
|
132
|
+
base.nullable = true;
|
|
133
|
+
return base;
|
|
134
|
+
}
|
|
135
|
+
function idProperty(resource) {
|
|
136
|
+
var _a;
|
|
137
|
+
const pk = resource.fields.find((f) => f.primaryKey);
|
|
138
|
+
if (pk)
|
|
139
|
+
return Object.assign(Object.assign({}, scalarProperty(pk.type)), { readOnly: true });
|
|
140
|
+
if ((_a = resource.sync) === null || _a === void 0 ? void 0 : _a.clientIds) {
|
|
141
|
+
return { type: "string", format: "uuid" };
|
|
142
|
+
}
|
|
143
|
+
return { type: "integer", readOnly: true };
|
|
144
|
+
}
|
|
145
|
+
function scalarProperty(type) {
|
|
146
|
+
switch (type) {
|
|
147
|
+
case "int":
|
|
148
|
+
return { type: "integer" };
|
|
149
|
+
case "float":
|
|
150
|
+
return { type: "number" };
|
|
151
|
+
case "bool":
|
|
152
|
+
return { type: "boolean" };
|
|
153
|
+
case "datetime":
|
|
154
|
+
return { type: "string", format: "date-time" };
|
|
155
|
+
case "uuid":
|
|
156
|
+
return { type: "string", format: "uuid" };
|
|
157
|
+
default:
|
|
158
|
+
return { type: "string" };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// -- operation helpers ------------------------------------------------------
|
|
162
|
+
function jsonResponse(status, schemaName) {
|
|
163
|
+
return {
|
|
164
|
+
[status]: {
|
|
165
|
+
description: "",
|
|
166
|
+
content: {
|
|
167
|
+
"application/json": {
|
|
168
|
+
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function jsonBody(schemaName) {
|
|
175
|
+
return {
|
|
176
|
+
required: true,
|
|
177
|
+
content: {
|
|
178
|
+
"application/json": {
|
|
179
|
+
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function idParam() {
|
|
185
|
+
return {
|
|
186
|
+
name: "id",
|
|
187
|
+
in: "path",
|
|
188
|
+
required: true,
|
|
189
|
+
schema: { type: "string" },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function queryParam(name, type) {
|
|
193
|
+
return { name, in: "query", required: false, schema: { type } };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Stamp the `x-rastack-sync*` vendor extensions on an operation of a local-mode
|
|
197
|
+
* resource. Ported from the old `python/rastack/spectacular.py` postprocess
|
|
198
|
+
* hook: confirmed operations become blocking client hooks, everything else on a
|
|
199
|
+
* local resource replicates.
|
|
200
|
+
*/
|
|
201
|
+
function syncStamp(operation, resource, verb) {
|
|
202
|
+
const sync = resource.sync;
|
|
203
|
+
if (!sync || sync.mode !== "local")
|
|
204
|
+
return operation;
|
|
205
|
+
operation["x-rastack-sync"] = isConfirmed(sync.confirmed, verb, operation)
|
|
206
|
+
? "confirmed"
|
|
207
|
+
: "local";
|
|
208
|
+
if (sync.load === "lazy")
|
|
209
|
+
operation["x-rastack-sync-load"] = "lazy";
|
|
210
|
+
if (sync.clientIds)
|
|
211
|
+
operation["x-rastack-sync-client-ids"] = true;
|
|
212
|
+
return operation;
|
|
213
|
+
}
|
|
214
|
+
function isConfirmed(confirmed, verb, operation) {
|
|
215
|
+
if (!confirmed || !confirmed.length)
|
|
216
|
+
return false;
|
|
217
|
+
for (const name of confirmed) {
|
|
218
|
+
if (name === verb)
|
|
219
|
+
return true;
|
|
220
|
+
if (name === "update" && verb === "partial_update")
|
|
221
|
+
return true;
|
|
222
|
+
if (operation.operationId && operation.operationId.includes(name)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
function pascal(str) {
|
|
229
|
+
return str
|
|
230
|
+
.split(/[-_]/)
|
|
231
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
232
|
+
.join("");
|
|
233
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Build a `ts.Program` (and thus a `TypeChecker`) over a set of resource
|
|
4
|
+
* definition files. The DSL package (`rad-tools/define`) is mapped to the
|
|
5
|
+
* in-tree/installed sources via `paths` so both the shipped package name and
|
|
6
|
+
* the local demo/tests resolve without extra linking.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createProgram(fileNames: string[]): ts.Program;
|
|
9
|
+
/** Every non-declaration source file the program was rooted at. */
|
|
10
|
+
export declare function resourceSourceFiles(program: ts.Program, fileNames: string[]): ts.SourceFile[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.resourceSourceFiles = exports.createProgram = void 0;
|
|
27
|
+
const path = __importStar(require("path"));
|
|
28
|
+
const ts = __importStar(require("typescript"));
|
|
29
|
+
/**
|
|
30
|
+
* Build a `ts.Program` (and thus a `TypeChecker`) over a set of resource
|
|
31
|
+
* definition files. The DSL package (`rad-tools/define`) is mapped to the
|
|
32
|
+
* in-tree/installed sources via `paths` so both the shipped package name and
|
|
33
|
+
* the local demo/tests resolve without extra linking.
|
|
34
|
+
*/
|
|
35
|
+
function createProgram(fileNames) {
|
|
36
|
+
const defineDir = path.resolve(__dirname, "../define");
|
|
37
|
+
const options = {
|
|
38
|
+
target: ts.ScriptTarget.ES2019,
|
|
39
|
+
module: ts.ModuleKind.CommonJS,
|
|
40
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
41
|
+
strict: true,
|
|
42
|
+
skipLibCheck: true,
|
|
43
|
+
esModuleInterop: true,
|
|
44
|
+
noEmit: true,
|
|
45
|
+
baseUrl: process.cwd(),
|
|
46
|
+
paths: {
|
|
47
|
+
"rad-tools/define": [defineDir],
|
|
48
|
+
"rad-tools/define/*": [defineDir + "/*"],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return ts.createProgram(fileNames, options);
|
|
52
|
+
}
|
|
53
|
+
exports.createProgram = createProgram;
|
|
54
|
+
/** Every non-declaration source file the program was rooted at. */
|
|
55
|
+
function resourceSourceFiles(program, fileNames) {
|
|
56
|
+
const wanted = new Set(fileNames.map((f) => path.resolve(f)));
|
|
57
|
+
return program
|
|
58
|
+
.getSourceFiles()
|
|
59
|
+
.filter((sf) => !sf.isDeclarationFile && wanted.has(path.resolve(sf.fileName)));
|
|
60
|
+
}
|
|
61
|
+
exports.resourceSourceFiles = resourceSourceFiles;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ColumnSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
type: "integer" | "float" | "boolean" | "date" | "datetime" | "string";
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
unique: boolean;
|
|
6
|
+
primaryKey: boolean;
|
|
7
|
+
pii: boolean;
|
|
8
|
+
piiReason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TableSchema {
|
|
11
|
+
tableName: string;
|
|
12
|
+
sourceFile: string;
|
|
13
|
+
columns: ColumnSchema[];
|
|
14
|
+
rowCount: number;
|
|
15
|
+
fingerprint: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseCsv(content: string): {
|
|
18
|
+
headers: string[];
|
|
19
|
+
rows: string[][];
|
|
20
|
+
};
|
|
21
|
+
export declare function inferSchema(csvPath: string, repoRoot: string): {
|
|
22
|
+
schema: TableSchema;
|
|
23
|
+
piiViolations: {
|
|
24
|
+
column: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
export declare function discoverCsvFiles(dir: string): string[];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverCsvFiles = exports.inferSchema = exports.parseCsv = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const minimatch_1 = require("minimatch");
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const DEFAULT_PII_PATTERNS = [
|
|
12
|
+
"*passport*",
|
|
13
|
+
"*_no",
|
|
14
|
+
"*_number",
|
|
15
|
+
"ssn",
|
|
16
|
+
"national_id",
|
|
17
|
+
"date_of_birth",
|
|
18
|
+
"dob",
|
|
19
|
+
"email",
|
|
20
|
+
"*phone*",
|
|
21
|
+
"*credit_card*",
|
|
22
|
+
"*card_number*",
|
|
23
|
+
"ip_address",
|
|
24
|
+
"latitude",
|
|
25
|
+
"longitude",
|
|
26
|
+
"*salary*",
|
|
27
|
+
"*income*",
|
|
28
|
+
];
|
|
29
|
+
function loadPiiConfig(repoRoot) {
|
|
30
|
+
const configPath = path_1.default.join(repoRoot, ".rad", "pii-patterns.yml");
|
|
31
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
32
|
+
return { patterns: DEFAULT_PII_PATTERNS, luhn_check: true, require_policy: true };
|
|
33
|
+
}
|
|
34
|
+
return js_yaml_1.default.load(fs_1.default.readFileSync(configPath, "utf-8"));
|
|
35
|
+
}
|
|
36
|
+
function isPiiColumn(name, config) {
|
|
37
|
+
const lower = name.toLowerCase();
|
|
38
|
+
for (const pattern of config.patterns) {
|
|
39
|
+
if ((0, minimatch_1.minimatch)(lower, pattern.toLowerCase())) {
|
|
40
|
+
return `matches PII pattern "${pattern}"`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function luhnCheck(value) {
|
|
46
|
+
const digits = value.replace(/\D/g, "");
|
|
47
|
+
if (digits.length < 13)
|
|
48
|
+
return false;
|
|
49
|
+
let sum = 0;
|
|
50
|
+
let alternate = false;
|
|
51
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
52
|
+
let n = parseInt(digits[i], 10);
|
|
53
|
+
if (alternate) {
|
|
54
|
+
n *= 2;
|
|
55
|
+
if (n > 9)
|
|
56
|
+
n -= 9;
|
|
57
|
+
}
|
|
58
|
+
sum += n;
|
|
59
|
+
alternate = !alternate;
|
|
60
|
+
}
|
|
61
|
+
return sum % 10 === 0;
|
|
62
|
+
}
|
|
63
|
+
function inferType(values) {
|
|
64
|
+
const nonEmpty = values.filter((v) => v.trim() !== "" && v.toLowerCase() !== "null");
|
|
65
|
+
if (nonEmpty.length === 0)
|
|
66
|
+
return "string";
|
|
67
|
+
if (nonEmpty.every((v) => /^\d+$/.test(v.trim())))
|
|
68
|
+
return "integer";
|
|
69
|
+
if (nonEmpty.every((v) => /^-?\d+(\.\d+)?$/.test(v.trim())))
|
|
70
|
+
return "float";
|
|
71
|
+
if (nonEmpty.every((v) => /^(true|false|yes|no|1|0)$/i.test(v.trim())))
|
|
72
|
+
return "boolean";
|
|
73
|
+
if (nonEmpty.every((v) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(v.trim())))
|
|
74
|
+
return "datetime";
|
|
75
|
+
if (nonEmpty.every((v) => /^\d{4}-\d{2}-\d{2}$/.test(v.trim())))
|
|
76
|
+
return "date";
|
|
77
|
+
return "string";
|
|
78
|
+
}
|
|
79
|
+
function parseCsv(content) {
|
|
80
|
+
const lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
81
|
+
if (lines.length === 0)
|
|
82
|
+
return { headers: [], rows: [] };
|
|
83
|
+
const splitLine = (line) => {
|
|
84
|
+
const result = [];
|
|
85
|
+
let current = "";
|
|
86
|
+
let inQuotes = false;
|
|
87
|
+
for (let i = 0; i < line.length; i++) {
|
|
88
|
+
if (line[i] === '"') {
|
|
89
|
+
inQuotes = !inQuotes;
|
|
90
|
+
}
|
|
91
|
+
else if (line[i] === "," && !inQuotes) {
|
|
92
|
+
result.push(current.trim());
|
|
93
|
+
current = "";
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
current += line[i];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
result.push(current.trim());
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
const headers = splitLine(lines[0]);
|
|
103
|
+
const rows = lines.slice(1).map(splitLine);
|
|
104
|
+
return { headers, rows };
|
|
105
|
+
}
|
|
106
|
+
exports.parseCsv = parseCsv;
|
|
107
|
+
function fingerprint(schema) {
|
|
108
|
+
const sig = schema.columns.map((c) => `${c.name}:${c.type}:${c.pii}`).join("|");
|
|
109
|
+
// Simple hash — not crypto, just change-detection
|
|
110
|
+
let h = 0;
|
|
111
|
+
for (let i = 0; i < sig.length; i++) {
|
|
112
|
+
h = (Math.imul(31, h) + sig.charCodeAt(i)) | 0;
|
|
113
|
+
}
|
|
114
|
+
return Math.abs(h).toString(16).padStart(8, "0");
|
|
115
|
+
}
|
|
116
|
+
function inferSchema(csvPath, repoRoot) {
|
|
117
|
+
const content = fs_1.default.readFileSync(csvPath, "utf-8");
|
|
118
|
+
const { headers, rows } = parseCsv(content);
|
|
119
|
+
const piiConfig = loadPiiConfig(repoRoot);
|
|
120
|
+
const tableName = path_1.default.basename(csvPath, ".csv");
|
|
121
|
+
const piiViolations = [];
|
|
122
|
+
const columns = headers.map((name, i) => {
|
|
123
|
+
const values = rows.map((r) => { var _a; return (_a = r[i]) !== null && _a !== void 0 ? _a : ""; });
|
|
124
|
+
const type = inferType(values);
|
|
125
|
+
const nullable = values.some((v) => v.trim() === "" || v.toLowerCase() === "null");
|
|
126
|
+
const nonEmpty = values.filter((v) => v.trim() !== "");
|
|
127
|
+
const unique = new Set(nonEmpty).size === nonEmpty.length;
|
|
128
|
+
const isPrimary = name.toLowerCase() === "id";
|
|
129
|
+
// PII: column name pattern match
|
|
130
|
+
let pii = false;
|
|
131
|
+
let piiReason;
|
|
132
|
+
const nameReason = isPiiColumn(name, piiConfig);
|
|
133
|
+
if (nameReason) {
|
|
134
|
+
pii = true;
|
|
135
|
+
piiReason = nameReason;
|
|
136
|
+
}
|
|
137
|
+
// PII: Luhn check on string columns that look numeric
|
|
138
|
+
if (!pii && piiConfig.luhn_check && type === "string") {
|
|
139
|
+
const luhnHits = values.filter((v) => v.trim() !== "" && luhnCheck(v));
|
|
140
|
+
if (luhnHits.length > values.length * 0.5) {
|
|
141
|
+
pii = true;
|
|
142
|
+
piiReason = "majority of values pass Luhn check (possible card numbers)";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (pii && piiReason) {
|
|
146
|
+
piiViolations.push({ column: name, reason: piiReason });
|
|
147
|
+
}
|
|
148
|
+
return { name, type, nullable, unique, primaryKey: isPrimary, pii, piiReason };
|
|
149
|
+
});
|
|
150
|
+
const base = { tableName, sourceFile: csvPath, columns, rowCount: rows.length };
|
|
151
|
+
return { schema: Object.assign(Object.assign({}, base), { fingerprint: fingerprint(base) }), piiViolations };
|
|
152
|
+
}
|
|
153
|
+
exports.inferSchema = inferSchema;
|
|
154
|
+
function discoverCsvFiles(dir) {
|
|
155
|
+
if (!fs_1.default.existsSync(dir))
|
|
156
|
+
return [];
|
|
157
|
+
return fs_1.default
|
|
158
|
+
.readdirSync(dir)
|
|
159
|
+
.filter((f) => f.endsWith(".csv"))
|
|
160
|
+
.map((f) => path_1.default.join(dir, f));
|
|
161
|
+
}
|
|
162
|
+
exports.discoverCsvFiles = discoverCsvFiles;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rad-tools authoring DSL — the TypeScript-first source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Resources are authored as plain TypeScript. The compiler
|
|
5
|
+
* (`tools/src/compile`) reads these definitions and, crucially, **infers
|
|
6
|
+
* foreign-key relations directly from the field types** via the TypeScript type
|
|
7
|
+
* checker: a field whose type is a {@link Ref} to another {@link ResourceType}
|
|
8
|
+
* is recorded as a foreign key, with the target app/model recovered from the
|
|
9
|
+
* `Ref`'s string-literal type arguments.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { resource, s } from "rad-tools/define";
|
|
13
|
+
*
|
|
14
|
+
* export const Airport = resource("airports", "airport", {
|
|
15
|
+
* code: s.string({ maxLength: 3, unique: true }),
|
|
16
|
+
* name: s.string({ maxLength: 120 }),
|
|
17
|
+
* }, { search: ["code", "name"], admin: { listDisplay: ["code", "name"] } });
|
|
18
|
+
*
|
|
19
|
+
* export const Terminal = resource("airports", "terminal", {
|
|
20
|
+
* airport: s.ref(() => Airport), // FK inferred from the type of this field
|
|
21
|
+
* label: s.string({ maxLength: 20 }),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/** A scalar field. The literal `__radScalar` type is what the compiler reads. */
|
|
26
|
+
export interface Scalar<T extends string> {
|
|
27
|
+
readonly __radScalar: T;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A foreign-key field. `App`/`Model` are carried as string-literal type
|
|
31
|
+
* arguments so the compiler can recover the relation target purely from types.
|
|
32
|
+
*/
|
|
33
|
+
export interface Ref<App extends string, Model extends string> {
|
|
34
|
+
readonly __radRef: true;
|
|
35
|
+
readonly __app: App;
|
|
36
|
+
readonly __model: Model;
|
|
37
|
+
}
|
|
38
|
+
export interface StringOptions {
|
|
39
|
+
maxLength?: number;
|
|
40
|
+
unique?: boolean;
|
|
41
|
+
primaryKey?: boolean;
|
|
42
|
+
default?: string;
|
|
43
|
+
null?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface NumberOptions {
|
|
46
|
+
unique?: boolean;
|
|
47
|
+
primaryKey?: boolean;
|
|
48
|
+
default?: number;
|
|
49
|
+
null?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface BoolOptions {
|
|
52
|
+
default?: boolean;
|
|
53
|
+
null?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface DateOptions {
|
|
56
|
+
autoNow?: boolean;
|
|
57
|
+
autoNowAdd?: boolean;
|
|
58
|
+
null?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Field builders. Each returns a phantom value whose *type* encodes the field
|
|
62
|
+
* kind; the runtime object is never used at compile time (the compiler analyses
|
|
63
|
+
* the source, it does not execute it) but is real so the DSL is also usable at
|
|
64
|
+
* runtime if a consumer wants it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const s: {
|
|
67
|
+
string(_opts?: StringOptions): Scalar<"string">;
|
|
68
|
+
int(_opts?: NumberOptions): Scalar<"int">;
|
|
69
|
+
float(_opts?: NumberOptions): Scalar<"float">;
|
|
70
|
+
bool(_opts?: BoolOptions): Scalar<"bool">;
|
|
71
|
+
datetime(_opts?: DateOptions): Scalar<"datetime">;
|
|
72
|
+
uuid(_opts?: StringOptions): Scalar<"uuid">;
|
|
73
|
+
/**
|
|
74
|
+
* A foreign key. The target is `() => SomeResource`; the compiler resolves
|
|
75
|
+
* the relation from the *type* `Ref<App, Model>` this returns — it does not
|
|
76
|
+
* need to run the thunk.
|
|
77
|
+
*/
|
|
78
|
+
ref<A extends string, M extends string>(_thunk: () => ResourceType<A, M, any>): Ref<A, M>;
|
|
79
|
+
};
|
|
80
|
+
export type AnyField = Scalar<string> | Ref<string, string>;
|
|
81
|
+
/** Local-first sync configuration (mirrors the old Python `class Sync:`). */
|
|
82
|
+
export interface SyncOptions {
|
|
83
|
+
mode?: "server" | "local";
|
|
84
|
+
scope?: string;
|
|
85
|
+
confirmed?: string[];
|
|
86
|
+
load?: "instant" | "lazy";
|
|
87
|
+
clientIds?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface ResourceOptions {
|
|
90
|
+
/** DRF-style search fields. */
|
|
91
|
+
search?: string[];
|
|
92
|
+
/** Orderable fields. */
|
|
93
|
+
ordering?: string[];
|
|
94
|
+
/** Admin config, or `false` to opt out of the admin. */
|
|
95
|
+
admin?: {
|
|
96
|
+
listDisplay?: string[];
|
|
97
|
+
} | false;
|
|
98
|
+
/** Local-first replication config. */
|
|
99
|
+
sync?: SyncOptions;
|
|
100
|
+
/** Access policy (secure by default). */
|
|
101
|
+
permission?: "authenticatedOrReadOnly" | "authenticated" | "public";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A resource definition. `App`/`Model` are preserved as string-literal type
|
|
105
|
+
* arguments so a {@link Ref} to this resource carries a recoverable target.
|
|
106
|
+
*/
|
|
107
|
+
export interface ResourceType<App extends string, Model extends string, F> {
|
|
108
|
+
readonly __rad: "resource";
|
|
109
|
+
readonly __app: App;
|
|
110
|
+
readonly __model: Model;
|
|
111
|
+
readonly app: App;
|
|
112
|
+
readonly model: Model;
|
|
113
|
+
readonly fields: F;
|
|
114
|
+
readonly options: ResourceOptions;
|
|
115
|
+
}
|
|
116
|
+
/** Define a resource. Capture app/model as literal types for FK inference. */
|
|
117
|
+
export declare function resource<App extends string, Model extends string, F extends Record<string, AnyField>>(app: App, model: Model, fields: F, options?: ResourceOptions): ResourceType<App, Model, F>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* rad-tools authoring DSL — the TypeScript-first source of truth.
|
|
4
|
+
*
|
|
5
|
+
* Resources are authored as plain TypeScript. The compiler
|
|
6
|
+
* (`tools/src/compile`) reads these definitions and, crucially, **infers
|
|
7
|
+
* foreign-key relations directly from the field types** via the TypeScript type
|
|
8
|
+
* checker: a field whose type is a {@link Ref} to another {@link ResourceType}
|
|
9
|
+
* is recorded as a foreign key, with the target app/model recovered from the
|
|
10
|
+
* `Ref`'s string-literal type arguments.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { resource, s } from "rad-tools/define";
|
|
14
|
+
*
|
|
15
|
+
* export const Airport = resource("airports", "airport", {
|
|
16
|
+
* code: s.string({ maxLength: 3, unique: true }),
|
|
17
|
+
* name: s.string({ maxLength: 120 }),
|
|
18
|
+
* }, { search: ["code", "name"], admin: { listDisplay: ["code", "name"] } });
|
|
19
|
+
*
|
|
20
|
+
* export const Terminal = resource("airports", "terminal", {
|
|
21
|
+
* airport: s.ref(() => Airport), // FK inferred from the type of this field
|
|
22
|
+
* label: s.string({ maxLength: 20 }),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.resource = exports.s = void 0;
|
|
28
|
+
/**
|
|
29
|
+
* Field builders. Each returns a phantom value whose *type* encodes the field
|
|
30
|
+
* kind; the runtime object is never used at compile time (the compiler analyses
|
|
31
|
+
* the source, it does not execute it) but is real so the DSL is also usable at
|
|
32
|
+
* runtime if a consumer wants it.
|
|
33
|
+
*/
|
|
34
|
+
exports.s = {
|
|
35
|
+
string(_opts) {
|
|
36
|
+
return { __radScalar: "string" };
|
|
37
|
+
},
|
|
38
|
+
int(_opts) {
|
|
39
|
+
return { __radScalar: "int" };
|
|
40
|
+
},
|
|
41
|
+
float(_opts) {
|
|
42
|
+
return { __radScalar: "float" };
|
|
43
|
+
},
|
|
44
|
+
bool(_opts) {
|
|
45
|
+
return { __radScalar: "bool" };
|
|
46
|
+
},
|
|
47
|
+
datetime(_opts) {
|
|
48
|
+
return { __radScalar: "datetime" };
|
|
49
|
+
},
|
|
50
|
+
uuid(_opts) {
|
|
51
|
+
return { __radScalar: "uuid" };
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* A foreign key. The target is `() => SomeResource`; the compiler resolves
|
|
55
|
+
* the relation from the *type* `Ref<App, Model>` this returns — it does not
|
|
56
|
+
* need to run the thunk.
|
|
57
|
+
*/
|
|
58
|
+
ref(_thunk) {
|
|
59
|
+
return { __radRef: true };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/** Define a resource. Capture app/model as literal types for FK inference. */
|
|
63
|
+
function resource(app, model, fields, options = {}) {
|
|
64
|
+
return {
|
|
65
|
+
__rad: "resource",
|
|
66
|
+
__app: app,
|
|
67
|
+
__model: model,
|
|
68
|
+
app,
|
|
69
|
+
model,
|
|
70
|
+
fields,
|
|
71
|
+
options,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
exports.resource = resource;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateDeleteAPIMethod(url: string, operationId: string, listQueryCacheKey?: string): [string, Set<string>];
|