@descryy/adapter-python 0.1.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/adapter.d.ts +75 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +481 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +117 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +347 -0
- package/dist/client.js.map +1 -0
- package/dist/extract.d.ts +231 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +2267 -0
- package/dist/extract.js.map +1 -0
- package/dist/extract.py +1319 -0
- package/dist/graphql.d.ts +197 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +302 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +99 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +211 -0
- package/dist/module.js.map +1 -0
- package/dist/orm.d.ts +102 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +221 -0
- package/dist/orm.js.map +1 -0
- package/dist/pydantic.d.ts +157 -0
- package/dist/pydantic.d.ts.map +1 -0
- package/dist/pydantic.js +318 -0
- package/dist/pydantic.js.map +1 -0
- package/dist/pytest-config.d.ts +60 -0
- package/dist/pytest-config.d.ts.map +1 -0
- package/dist/pytest-config.js +147 -0
- package/dist/pytest-config.js.map +1 -0
- package/dist/routes.d.ts +249 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +511 -0
- package/dist/routes.js.map +1 -0
- package/package.json +34 -0
- package/src/extract.py +1319 -0
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python module paths and import resolution — the whole of R1.
|
|
3
|
+
*
|
|
4
|
+
* ## Where the last adapter lost three quarters of its graph
|
|
5
|
+
*
|
|
6
|
+
* DEC-059: the TypeScript adapter ignored the repository's own path
|
|
7
|
+
* configuration, so every aliased import failed and every cross-directory edge
|
|
8
|
+
* with it. Python's equivalents are `src/` layouts, `PYTHONPATH`, namespace
|
|
9
|
+
* packages and `pyproject.toml`'s package table — **the same defect in a
|
|
10
|
+
* different spelling** — so the layout is read from the repository here rather
|
|
11
|
+
* than assumed, and what could not be read is disclosed.
|
|
12
|
+
*
|
|
13
|
+
* ## The two hard cases, both in the corpus
|
|
14
|
+
*
|
|
15
|
+
* `from . import validate_order` — level 1, **no module name**. The dots are the
|
|
16
|
+
* whole address, and they are not recoverable from the specifier text: this is
|
|
17
|
+
* why the Python side reports `ImportFrom.level` rather than a string. It
|
|
18
|
+
* resolves to the containing package's `__init__.py`, which is the re-export hop
|
|
19
|
+
* golden pattern 03 turns on.
|
|
20
|
+
*
|
|
21
|
+
* `from .money import format_money` — level 1 with a module. Resolves to a
|
|
22
|
+
* sibling file, and the imported name is a *symbol* in it rather than a module.
|
|
23
|
+
* Deciding which of the two a name is cannot be done from the import alone —
|
|
24
|
+
* `from .pkg import mod` and `from .mod import fn` are identical in shape — so
|
|
25
|
+
* both candidates are returned and the caller picks by what exists.
|
|
26
|
+
*/
|
|
27
|
+
/** `src/order_service.py` -> `["src", "order_service"]`; `src/__init__.py` -> `["src"]`. */
|
|
28
|
+
export declare function modulePathOf(repoRelativeFile: string): readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The display name of a module node.
|
|
31
|
+
*
|
|
32
|
+
* A package takes its directory's name — the corpus binds the `barrel` role to
|
|
33
|
+
* the symbol `src` at `src/__init__.py`, so `__init__` would fail to bind and
|
|
34
|
+
* the failure would read as a missing node rather than as a naming choice.
|
|
35
|
+
*/
|
|
36
|
+
export declare function moduleNameOf(repoRelativeFile: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* The project's distribution name, read rather than guessed.
|
|
39
|
+
*
|
|
40
|
+
* `pyproject.toml` first, then `setup.cfg`. Parsed with a narrow regex on
|
|
41
|
+
* purpose: a TOML dependency would buy correctness on a field nothing else in
|
|
42
|
+
* this adapter reads, and the fallback is disclosed through
|
|
43
|
+
* `capabilities().nodeIdentityFallback` either way.
|
|
44
|
+
*/
|
|
45
|
+
export declare function projectName(absoluteRoot: string): {
|
|
46
|
+
name: string;
|
|
47
|
+
evidence: string;
|
|
48
|
+
};
|
|
49
|
+
export interface ImportRecord {
|
|
50
|
+
readonly kind: string;
|
|
51
|
+
readonly level: number;
|
|
52
|
+
readonly module: string | null;
|
|
53
|
+
readonly name: string | null;
|
|
54
|
+
readonly local: string;
|
|
55
|
+
readonly line: number;
|
|
56
|
+
}
|
|
57
|
+
/** What an import might refer to: a module file, or a symbol inside one. */
|
|
58
|
+
export interface ImportTarget {
|
|
59
|
+
/** Repo-relative file the specifier reaches, or undefined if nothing does. */
|
|
60
|
+
readonly file: string | undefined;
|
|
61
|
+
/** The symbol name inside that file, when the import names one. */
|
|
62
|
+
readonly symbol: string | undefined;
|
|
63
|
+
/** Set when the specifier names a package whose `__init__.py` re-exports. */
|
|
64
|
+
readonly viaPackageInit: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface ResolveOptions {
|
|
67
|
+
/** Repo-relative files this run analysed. Nothing outside it can be a target. */
|
|
68
|
+
readonly known: ReadonlySet<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Directories that are import roots, repo-relative, longest first.
|
|
71
|
+
*
|
|
72
|
+
* A `src/` layout means `from myapp.models import X` resolves under `src/`,
|
|
73
|
+
* not under the repository root. Discovered rather than assumed — see
|
|
74
|
+
* `importRoots`.
|
|
75
|
+
*/
|
|
76
|
+
readonly roots: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve one import to a file, and to a symbol inside it when it names one.
|
|
80
|
+
*
|
|
81
|
+
* Returns `file: undefined` for anything outside the analysed set — the standard
|
|
82
|
+
* library, an installed package, a namespace package this run did not read.
|
|
83
|
+
* That is a scope boundary rather than a failure, and DEC-050 keeps the two
|
|
84
|
+
* apart in the ledger.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveImport(record: ImportRecord, fromFile: string, options: ResolveOptions): ImportTarget;
|
|
87
|
+
/**
|
|
88
|
+
* Directories an absolute import can be resolved against.
|
|
89
|
+
*
|
|
90
|
+
* This is the DEC-059 surface for Python, and it is *derived from the files that
|
|
91
|
+
* exist* rather than from a configuration file that may not. A directory is an
|
|
92
|
+
* import root when it directly contains a package — a subdirectory with an
|
|
93
|
+
* `__init__.py` — or a top-level module. `src/` layouts fall out of that without
|
|
94
|
+
* being special-cased, and so does a repository with no layout at all.
|
|
95
|
+
*
|
|
96
|
+
* Longest first, so a `src/` layout wins over the root when both could match.
|
|
97
|
+
*/
|
|
98
|
+
export declare function importRoots(files: readonly string[]): readonly string[];
|
|
99
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAQxE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAwBpF;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAeD,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,GACtB,YAAY,CA0Dd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAwCvE"}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python module paths and import resolution — the whole of R1.
|
|
3
|
+
*
|
|
4
|
+
* ## Where the last adapter lost three quarters of its graph
|
|
5
|
+
*
|
|
6
|
+
* DEC-059: the TypeScript adapter ignored the repository's own path
|
|
7
|
+
* configuration, so every aliased import failed and every cross-directory edge
|
|
8
|
+
* with it. Python's equivalents are `src/` layouts, `PYTHONPATH`, namespace
|
|
9
|
+
* packages and `pyproject.toml`'s package table — **the same defect in a
|
|
10
|
+
* different spelling** — so the layout is read from the repository here rather
|
|
11
|
+
* than assumed, and what could not be read is disclosed.
|
|
12
|
+
*
|
|
13
|
+
* ## The two hard cases, both in the corpus
|
|
14
|
+
*
|
|
15
|
+
* `from . import validate_order` — level 1, **no module name**. The dots are the
|
|
16
|
+
* whole address, and they are not recoverable from the specifier text: this is
|
|
17
|
+
* why the Python side reports `ImportFrom.level` rather than a string. It
|
|
18
|
+
* resolves to the containing package's `__init__.py`, which is the re-export hop
|
|
19
|
+
* golden pattern 03 turns on.
|
|
20
|
+
*
|
|
21
|
+
* `from .money import format_money` — level 1 with a module. Resolves to a
|
|
22
|
+
* sibling file, and the imported name is a *symbol* in it rather than a module.
|
|
23
|
+
* Deciding which of the two a name is cannot be done from the import alone —
|
|
24
|
+
* `from .pkg import mod` and `from .mod import fn` are identical in shape — so
|
|
25
|
+
* both candidates are returned and the caller picks by what exists.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
/** `src/order_service.py` -> `["src", "order_service"]`; `src/__init__.py` -> `["src"]`. */
|
|
30
|
+
export function modulePathOf(repoRelativeFile) {
|
|
31
|
+
const withoutExtension = repoRelativeFile.replace(/\.pyi?$/, "");
|
|
32
|
+
const parts = withoutExtension.split("/").filter((p) => p !== "" && p !== ".");
|
|
33
|
+
// A package *is* its directory. `src/__init__.py` and the directory `src` are
|
|
34
|
+
// one module, and emitting two nodes for them would split every import into
|
|
35
|
+
// the package across two targets.
|
|
36
|
+
if (parts[parts.length - 1] === "__init__")
|
|
37
|
+
parts.pop();
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The display name of a module node.
|
|
42
|
+
*
|
|
43
|
+
* A package takes its directory's name — the corpus binds the `barrel` role to
|
|
44
|
+
* the symbol `src` at `src/__init__.py`, so `__init__` would fail to bind and
|
|
45
|
+
* the failure would read as a missing node rather than as a naming choice.
|
|
46
|
+
*/
|
|
47
|
+
export function moduleNameOf(repoRelativeFile) {
|
|
48
|
+
const path = modulePathOf(repoRelativeFile);
|
|
49
|
+
return path[path.length - 1] ?? "";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The project's distribution name, read rather than guessed.
|
|
53
|
+
*
|
|
54
|
+
* `pyproject.toml` first, then `setup.cfg`. Parsed with a narrow regex on
|
|
55
|
+
* purpose: a TOML dependency would buy correctness on a field nothing else in
|
|
56
|
+
* this adapter reads, and the fallback is disclosed through
|
|
57
|
+
* `capabilities().nodeIdentityFallback` either way.
|
|
58
|
+
*/
|
|
59
|
+
export function projectName(absoluteRoot) {
|
|
60
|
+
const pyproject = join(absoluteRoot, "pyproject.toml");
|
|
61
|
+
if (existsSync(pyproject)) {
|
|
62
|
+
const text = readFileSync(pyproject, "utf8");
|
|
63
|
+
const inProject = /\[project\][^[]*?^\s*name\s*=\s*["']([^"']+)["']/ms.exec(text);
|
|
64
|
+
const inPoetry = /\[tool\.poetry\][^[]*?^\s*name\s*=\s*["']([^"']+)["']/ms.exec(text);
|
|
65
|
+
const found = inProject?.[1] ?? inPoetry?.[1];
|
|
66
|
+
if (found !== undefined)
|
|
67
|
+
return { name: found, evidence: `pyproject.toml declares ${found}` };
|
|
68
|
+
}
|
|
69
|
+
const setupCfg = join(absoluteRoot, "setup.cfg");
|
|
70
|
+
if (existsSync(setupCfg)) {
|
|
71
|
+
const found = /^\s*name\s*=\s*(.+)$/m.exec(readFileSync(setupCfg, "utf8"));
|
|
72
|
+
if (found?.[1] !== undefined) {
|
|
73
|
+
return { name: found[1].trim(), evidence: `setup.cfg declares ${found[1].trim()}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name: ".",
|
|
78
|
+
// The same shape of admission DEC-024 required of the TypeScript adapter:
|
|
79
|
+
// without a declared name, node identity rests on the module path alone.
|
|
80
|
+
evidence: "no pyproject.toml or setup.cfg declares a project name, so node identity falls back to " +
|
|
81
|
+
"the module path alone (DEC-024, DEC-054)",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Candidate files for a dotted module path, in Python's own order.
|
|
86
|
+
*
|
|
87
|
+
* A package's `__init__.py` is checked before a same-named module, because that
|
|
88
|
+
* is what the interpreter does and a graph that disagrees with the interpreter
|
|
89
|
+
* is wrong regardless of which is nicer.
|
|
90
|
+
*/
|
|
91
|
+
function candidates(parts) {
|
|
92
|
+
if (parts.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const base = parts.join("/");
|
|
95
|
+
return [`${base}/__init__.py`, `${base}.py`, `${base}.pyi`];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve one import to a file, and to a symbol inside it when it names one.
|
|
99
|
+
*
|
|
100
|
+
* Returns `file: undefined` for anything outside the analysed set — the standard
|
|
101
|
+
* library, an installed package, a namespace package this run did not read.
|
|
102
|
+
* That is a scope boundary rather than a failure, and DEC-050 keeps the two
|
|
103
|
+
* apart in the ledger.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveImport(record, fromFile, options) {
|
|
106
|
+
const miss = { file: undefined, symbol: undefined, viaPackageInit: false };
|
|
107
|
+
if (record.level > 0) {
|
|
108
|
+
// Relative. `level` counts dots: 1 is the containing package, 2 its parent.
|
|
109
|
+
const here = modulePathOf(fromFile);
|
|
110
|
+
// A module inside a package is addressed relative to the package, so the
|
|
111
|
+
// module's own last segment is dropped first unless it *is* the package.
|
|
112
|
+
const isPackage = fromFile.endsWith("__init__.py");
|
|
113
|
+
const packagePath = isPackage ? [...here] : here.slice(0, -1);
|
|
114
|
+
const base = packagePath.slice(0, packagePath.length - (record.level - 1));
|
|
115
|
+
if (base.length === 0 && record.level > 1)
|
|
116
|
+
return miss;
|
|
117
|
+
const parts = record.module === null ? base : [...base, ...record.module.split(".")];
|
|
118
|
+
// `from . import x` and `from .mod import x`: x may be a submodule or a
|
|
119
|
+
// symbol. The submodule is tried first because it is unambiguous when it
|
|
120
|
+
// exists; a symbol needs the package's `__init__` to be the file.
|
|
121
|
+
if (record.name !== null) {
|
|
122
|
+
const asModule = candidates([...parts, record.name]).find((c) => options.known.has(c));
|
|
123
|
+
if (asModule !== undefined) {
|
|
124
|
+
return { file: asModule, symbol: undefined, viaPackageInit: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const asFile = candidates(parts).find((c) => options.known.has(c));
|
|
128
|
+
if (asFile === undefined)
|
|
129
|
+
return miss;
|
|
130
|
+
return {
|
|
131
|
+
file: asFile,
|
|
132
|
+
symbol: record.name ?? undefined,
|
|
133
|
+
viaPackageInit: asFile.endsWith("__init__.py"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Absolute: try each import root. `import a.b.c` binds `a`, but the file the
|
|
137
|
+
// specifier names is still `a/b/c`, so the whole dotted path is resolved.
|
|
138
|
+
const dotted = (record.module ?? "").split(".").filter((p) => p !== "");
|
|
139
|
+
if (dotted.length === 0)
|
|
140
|
+
return miss;
|
|
141
|
+
for (const root of options.roots) {
|
|
142
|
+
const prefix = root === "" ? [] : root.split("/");
|
|
143
|
+
if (record.name !== null) {
|
|
144
|
+
const asModule = candidates([...prefix, ...dotted, record.name]).find((c) => options.known.has(c));
|
|
145
|
+
if (asModule !== undefined) {
|
|
146
|
+
return { file: asModule, symbol: undefined, viaPackageInit: false };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const asFile = candidates([...prefix, ...dotted]).find((c) => options.known.has(c));
|
|
150
|
+
if (asFile !== undefined) {
|
|
151
|
+
return {
|
|
152
|
+
file: asFile,
|
|
153
|
+
symbol: record.name ?? undefined,
|
|
154
|
+
viaPackageInit: asFile.endsWith("__init__.py"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return miss;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Directories an absolute import can be resolved against.
|
|
162
|
+
*
|
|
163
|
+
* This is the DEC-059 surface for Python, and it is *derived from the files that
|
|
164
|
+
* exist* rather than from a configuration file that may not. A directory is an
|
|
165
|
+
* import root when it directly contains a package — a subdirectory with an
|
|
166
|
+
* `__init__.py` — or a top-level module. `src/` layouts fall out of that without
|
|
167
|
+
* being special-cased, and so does a repository with no layout at all.
|
|
168
|
+
*
|
|
169
|
+
* Longest first, so a `src/` layout wins over the root when both could match.
|
|
170
|
+
*/
|
|
171
|
+
export function importRoots(files) {
|
|
172
|
+
// **A Set, because `files.includes` here was quadratic in the repository.**
|
|
173
|
+
//
|
|
174
|
+
// This ran a linear scan of every file in the run, inside a loop over every
|
|
175
|
+
// file, inside a walk up each path — hundreds of millions of string
|
|
176
|
+
// comparisons on a large repository, and worse the deeper its packages nest.
|
|
177
|
+
// Measured: posthog took 1,655 s against 112 s for home-assistant-core at the
|
|
178
|
+
// same file count (18,479 against 18,189). It reads as a repository-specific
|
|
179
|
+
// slowdown and is nothing of the kind — every repository paid it, in
|
|
180
|
+
// proportion to its size squared.
|
|
181
|
+
const present = new Set(files);
|
|
182
|
+
const roots = new Set();
|
|
183
|
+
// Directories walk out to the same root repeatedly; caching by directory turns
|
|
184
|
+
// the remaining per-file walk into one walk per distinct directory.
|
|
185
|
+
const rootOfDirectory = new Map();
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const parts = file.split("/");
|
|
188
|
+
if (parts.length === 1) {
|
|
189
|
+
roots.add("");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const directory = parts.slice(0, -1).join("/");
|
|
193
|
+
const cached = rootOfDirectory.get(directory);
|
|
194
|
+
if (cached !== undefined) {
|
|
195
|
+
roots.add(cached);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// The outermost directory that is NOT a package is the import root: walking
|
|
199
|
+
// out of `src/myapp/models.py` stops at `src`, because `src` has no
|
|
200
|
+
// `__init__.py` while `src/myapp` does.
|
|
201
|
+
let depth = parts.length - 1;
|
|
202
|
+
while (depth > 0 && present.has(`${parts.slice(0, depth).join("/")}/__init__.py`)) {
|
|
203
|
+
depth -= 1;
|
|
204
|
+
}
|
|
205
|
+
const root = parts.slice(0, depth).join("/");
|
|
206
|
+
rootOfDirectory.set(directory, root);
|
|
207
|
+
roots.add(root);
|
|
208
|
+
}
|
|
209
|
+
return [...roots].sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
|
package/dist/orm.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ORM framework extractor for Python — SQLAlchemy and Django.
|
|
3
|
+
*
|
|
4
|
+
* **A `MODEL` is a framework claim, and this file is where the claim is earned.**
|
|
5
|
+
* Golden pattern 07 states the constraint that produced it: in the conformance
|
|
6
|
+
* corpus `Order` and `CreateOrderRequest` are structurally identical, so any
|
|
7
|
+
* classification a *language* adapter made between them would be a name
|
|
8
|
+
* heuristic — the one thing pattern 12 exists to punish. The evidence that
|
|
9
|
+
* separates them is a persistence mapping: a declarative base, a table binding,
|
|
10
|
+
* a column declaration. All three are written in the source, and this file reads
|
|
11
|
+
* them rather than inferring them.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is a name rule. `isTestSuiteClass` in `extract.ts` deliberately
|
|
14
|
+
* keeps one — a base whose name ends in `TestCase` — because the cost of being
|
|
15
|
+
* wrong is a test attributed to the wrong suite. The cost of being wrong here is
|
|
16
|
+
* a `MODEL` node that no database backs, which corrupts every data-propagation
|
|
17
|
+
* traversal above it, so provenance or a chain to provenance is the only
|
|
18
|
+
* admission. A class named `Model` proves nothing.
|
|
19
|
+
*
|
|
20
|
+
* The split with `extract.py` is the same one the rest of this adapter uses: the
|
|
21
|
+
* Python side records syntactic form and names no framework, and every decision
|
|
22
|
+
* about what a form *means* is taken here.
|
|
23
|
+
*/
|
|
24
|
+
/** One assignment in a class body, as `extract.py` recorded it. */
|
|
25
|
+
export interface PyAttribute {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly annotation: string | null;
|
|
28
|
+
/** The callee as written — `Column`, `mapped_column`, `models.CharField`. */
|
|
29
|
+
readonly callee: string | null;
|
|
30
|
+
readonly args: readonly string[];
|
|
31
|
+
/** Keyword arguments whose value is a written constant. */
|
|
32
|
+
readonly keywords: Readonly<Record<string, unknown>>;
|
|
33
|
+
/** Keyword arguments that were written with a value this reader cannot state. */
|
|
34
|
+
readonly opaqueKeywords: readonly string[];
|
|
35
|
+
readonly literal: unknown;
|
|
36
|
+
readonly hasLiteral: boolean;
|
|
37
|
+
readonly line: number;
|
|
38
|
+
}
|
|
39
|
+
export type OrmFramework = "sqlalchemy" | "django";
|
|
40
|
+
export interface OrmField {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly nullable: boolean;
|
|
43
|
+
/** Where the answer came from, so a default can be told from a declaration. */
|
|
44
|
+
readonly nullableFrom: "declared" | "annotation" | "framework-default";
|
|
45
|
+
}
|
|
46
|
+
export interface OrmShape {
|
|
47
|
+
readonly framework: OrmFramework;
|
|
48
|
+
/** The mapped table, where the source names one. */
|
|
49
|
+
readonly table: string | null;
|
|
50
|
+
readonly fields: readonly OrmField[];
|
|
51
|
+
/**
|
|
52
|
+
* Fields whose nullability is written in a form this reader cannot evaluate.
|
|
53
|
+
*
|
|
54
|
+
* Listed rather than guessed, and listed rather than dropped. `nullable=FLAG`
|
|
55
|
+
* is a stated answer this reader failed to read, and letting the framework
|
|
56
|
+
* default stand in for it would report the opposite of the source as fact.
|
|
57
|
+
*/
|
|
58
|
+
readonly undecided: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Modules whose names, if a base class came from one, make the framework certain.
|
|
62
|
+
*
|
|
63
|
+
* Provenance is written in the import statement and needs no resolution, which
|
|
64
|
+
* is why a model is recognisable at R0. Only the cross-file *chain* — a project's
|
|
65
|
+
* own `class BaseModel(Base)` — needs imports resolved.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ORM_BASE_MODULES: Readonly<Record<OrmFramework, ReadonlySet<string>>>;
|
|
68
|
+
/**
|
|
69
|
+
* Calls that *produce* a declarative base rather than being one.
|
|
70
|
+
*
|
|
71
|
+
* SQLAlchemy 1.x writes `Base = declarative_base()` at module level, so the base
|
|
72
|
+
* class of a model is a name bound to a call result and not a class declaration
|
|
73
|
+
* at all. Without this rule every 1.x model in existence is invisible.
|
|
74
|
+
*/
|
|
75
|
+
export declare const DECLARATIVE_BASE_FACTORIES: ReadonlySet<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Is this attribute a mapped column, and by which framework's rule?
|
|
78
|
+
*
|
|
79
|
+
* Django's rule is shape-based rather than a name list: every field class in
|
|
80
|
+
* `django.db.models` ends in `Field`, and new ones are added by Django and by
|
|
81
|
+
* every third-party app. A closed list would go stale on each release and would
|
|
82
|
+
* miss `JSONField`, `ArrayField` and every custom field a project writes. The
|
|
83
|
+
* receiver still has to be the imported `models` module, so this is not a name
|
|
84
|
+
* heuristic — the provenance is checked by the caller before this is consulted.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isColumnCall(attribute: PyAttribute, framework: OrmFramework): boolean;
|
|
87
|
+
/** A base class name that is the framework's own, rather than a project's. */
|
|
88
|
+
export declare function isFrameworkBaseName(name: string, framework: OrmFramework): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* The mapped shape of a class already established to belong to `framework`.
|
|
91
|
+
*
|
|
92
|
+
* Returns `null` where the class declares no column at all. That guard is what
|
|
93
|
+
* keeps a declarative base out of the graph as a model of its own: `class
|
|
94
|
+
* Base(DeclarativeBase): pass` inherits the provenance and maps nothing, and a
|
|
95
|
+
* `MODEL` with no fields is a claim about a table that does not exist.
|
|
96
|
+
*/
|
|
97
|
+
export declare function ormShapeOf(input: {
|
|
98
|
+
readonly framework: OrmFramework;
|
|
99
|
+
readonly attributes: readonly PyAttribute[];
|
|
100
|
+
readonly meta: readonly PyAttribute[];
|
|
101
|
+
}): OrmShape | null;
|
|
102
|
+
//# sourceMappingURL=orm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orm.d.ts","sourceRoot":"","sources":["../src/orm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,iFAAiF;IACjF,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEnD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,UAAU,GAAG,YAAY,GAAG,mBAAmB,CAAC;CACxE;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAQhF,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAIzD,CAAC;AA2CH;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,GAAG,OAAO,CAMrF;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,OAAO,CAElF;AA8DD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;CACvC,GAAG,QAAQ,GAAG,IAAI,CA0ClB"}
|
package/dist/orm.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ORM framework extractor for Python — SQLAlchemy and Django.
|
|
3
|
+
*
|
|
4
|
+
* **A `MODEL` is a framework claim, and this file is where the claim is earned.**
|
|
5
|
+
* Golden pattern 07 states the constraint that produced it: in the conformance
|
|
6
|
+
* corpus `Order` and `CreateOrderRequest` are structurally identical, so any
|
|
7
|
+
* classification a *language* adapter made between them would be a name
|
|
8
|
+
* heuristic — the one thing pattern 12 exists to punish. The evidence that
|
|
9
|
+
* separates them is a persistence mapping: a declarative base, a table binding,
|
|
10
|
+
* a column declaration. All three are written in the source, and this file reads
|
|
11
|
+
* them rather than inferring them.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is a name rule. `isTestSuiteClass` in `extract.ts` deliberately
|
|
14
|
+
* keeps one — a base whose name ends in `TestCase` — because the cost of being
|
|
15
|
+
* wrong is a test attributed to the wrong suite. The cost of being wrong here is
|
|
16
|
+
* a `MODEL` node that no database backs, which corrupts every data-propagation
|
|
17
|
+
* traversal above it, so provenance or a chain to provenance is the only
|
|
18
|
+
* admission. A class named `Model` proves nothing.
|
|
19
|
+
*
|
|
20
|
+
* The split with `extract.py` is the same one the rest of this adapter uses: the
|
|
21
|
+
* Python side records syntactic form and names no framework, and every decision
|
|
22
|
+
* about what a form *means* is taken here.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Modules whose names, if a base class came from one, make the framework certain.
|
|
26
|
+
*
|
|
27
|
+
* Provenance is written in the import statement and needs no resolution, which
|
|
28
|
+
* is why a model is recognisable at R0. Only the cross-file *chain* — a project's
|
|
29
|
+
* own `class BaseModel(Base)` — needs imports resolved.
|
|
30
|
+
*/
|
|
31
|
+
export const ORM_BASE_MODULES = {
|
|
32
|
+
sqlalchemy: new Set([
|
|
33
|
+
"sqlalchemy.orm",
|
|
34
|
+
"sqlalchemy.orm.decl_api",
|
|
35
|
+
"sqlalchemy.ext.declarative",
|
|
36
|
+
"sqlalchemy.ext.declarative.api",
|
|
37
|
+
]),
|
|
38
|
+
django: new Set(["django.db", "django.db.models", "django.db.models.base"]),
|
|
39
|
+
};
|
|
40
|
+
/** The framework's own base class names. A project's own base reaches these by chain. */
|
|
41
|
+
const ORM_BASE_NAMES = {
|
|
42
|
+
// `Base` is deliberately absent. It is what almost every SQLAlchemy project
|
|
43
|
+
// calls its declarative base, and admitting it would be a name rule wearing a
|
|
44
|
+
// framework's clothes — a project is free to call its base anything, and
|
|
45
|
+
// something else is free to be called `Base`. The 1.x shape is reached through
|
|
46
|
+
// `DECLARATIVE_BASE_FACTORIES` instead, which is provenance.
|
|
47
|
+
sqlalchemy: new Set(["DeclarativeBase"]),
|
|
48
|
+
django: new Set(["Model"]),
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Calls that *produce* a declarative base rather than being one.
|
|
52
|
+
*
|
|
53
|
+
* SQLAlchemy 1.x writes `Base = declarative_base()` at module level, so the base
|
|
54
|
+
* class of a model is a name bound to a call result and not a class declaration
|
|
55
|
+
* at all. Without this rule every 1.x model in existence is invisible.
|
|
56
|
+
*/
|
|
57
|
+
export const DECLARATIVE_BASE_FACTORIES = new Set([
|
|
58
|
+
"declarative_base",
|
|
59
|
+
"as_declarative",
|
|
60
|
+
"registry",
|
|
61
|
+
]);
|
|
62
|
+
/** Column constructors, per framework. */
|
|
63
|
+
const COLUMN_CALLS = {
|
|
64
|
+
sqlalchemy: new Set(["Column", "mapped_column", "deferred", "synonym_for"]),
|
|
65
|
+
/**
|
|
66
|
+
* Django's relational fields, which are the exception to the `…Field` shape
|
|
67
|
+
* below and were **not** guessed at — they were counted.
|
|
68
|
+
*
|
|
69
|
+
* Across django's own tree `ForeignKey` is the second most common field
|
|
70
|
+
* constructor in existence, 914 uses against `CharField`'s 1,226, and the
|
|
71
|
+
* shape rule alone missed every one of them because it is the one field class
|
|
72
|
+
* Django did not suffix. `GenericForeignKey` and `GenericRelation` came out of
|
|
73
|
+
* the same count at 59 and 46.
|
|
74
|
+
*/
|
|
75
|
+
django: new Set([
|
|
76
|
+
"ForeignKey",
|
|
77
|
+
"OneToOneField",
|
|
78
|
+
"ManyToManyField",
|
|
79
|
+
"GenericForeignKey",
|
|
80
|
+
"GenericRelation",
|
|
81
|
+
]),
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Attribute names that are configuration rather than columns.
|
|
85
|
+
*
|
|
86
|
+
* `__tablename__` is the table binding and `__table_args__` its options; neither
|
|
87
|
+
* is a field, and reporting them as fields would put two members in every model
|
|
88
|
+
* that no query can select.
|
|
89
|
+
*/
|
|
90
|
+
const NOT_A_FIELD = new Set([
|
|
91
|
+
"__tablename__",
|
|
92
|
+
"__table_args__",
|
|
93
|
+
"__mapper_args__",
|
|
94
|
+
"__abstract__",
|
|
95
|
+
"objects",
|
|
96
|
+
"Meta",
|
|
97
|
+
]);
|
|
98
|
+
const leafOf = (dotted) => dotted === null ? "" : (dotted.split(".").pop() ?? "");
|
|
99
|
+
/**
|
|
100
|
+
* Is this attribute a mapped column, and by which framework's rule?
|
|
101
|
+
*
|
|
102
|
+
* Django's rule is shape-based rather than a name list: every field class in
|
|
103
|
+
* `django.db.models` ends in `Field`, and new ones are added by Django and by
|
|
104
|
+
* every third-party app. A closed list would go stale on each release and would
|
|
105
|
+
* miss `JSONField`, `ArrayField` and every custom field a project writes. The
|
|
106
|
+
* receiver still has to be the imported `models` module, so this is not a name
|
|
107
|
+
* heuristic — the provenance is checked by the caller before this is consulted.
|
|
108
|
+
*/
|
|
109
|
+
export function isColumnCall(attribute, framework) {
|
|
110
|
+
if (attribute.callee === null)
|
|
111
|
+
return false;
|
|
112
|
+
const leaf = leafOf(attribute.callee);
|
|
113
|
+
if (COLUMN_CALLS[framework].has(leaf))
|
|
114
|
+
return true;
|
|
115
|
+
if (framework === "sqlalchemy")
|
|
116
|
+
return false;
|
|
117
|
+
return leaf.endsWith("Field") && leaf.length > "Field".length;
|
|
118
|
+
}
|
|
119
|
+
/** A base class name that is the framework's own, rather than a project's. */
|
|
120
|
+
export function isFrameworkBaseName(name, framework) {
|
|
121
|
+
return ORM_BASE_NAMES[framework].has(leafOf(name));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Nullability for one mapped column.
|
|
125
|
+
*
|
|
126
|
+
* Order is the framework's own, not this reader's preference. An explicit
|
|
127
|
+
* keyword is the source's answer and wins outright. A primary key is not
|
|
128
|
+
* nullable — that is SQL, not a convention. SQLAlchemy 2.0 then derives
|
|
129
|
+
* nullability from the `Mapped[...]` annotation, and only when none of those
|
|
130
|
+
* spoke does the framework default apply.
|
|
131
|
+
*/
|
|
132
|
+
function nullabilityOf(attribute, framework) {
|
|
133
|
+
const keyword = framework === "django" ? "null" : "nullable";
|
|
134
|
+
if (attribute.opaqueKeywords.includes(keyword))
|
|
135
|
+
return { undecided: true };
|
|
136
|
+
const declared = attribute.keywords[keyword];
|
|
137
|
+
if (typeof declared === "boolean") {
|
|
138
|
+
return { name: attribute.name, nullable: declared, nullableFrom: "declared" };
|
|
139
|
+
}
|
|
140
|
+
if (attribute.keywords["primary_key"] === true) {
|
|
141
|
+
return { name: attribute.name, nullable: false, nullableFrom: "declared" };
|
|
142
|
+
}
|
|
143
|
+
if (framework === "django") {
|
|
144
|
+
// Django's documented default, and the only one it has: a field is NOT NULL
|
|
145
|
+
// unless the model says otherwise.
|
|
146
|
+
return { name: attribute.name, nullable: false, nullableFrom: "framework-default" };
|
|
147
|
+
}
|
|
148
|
+
// SQLAlchemy 2.0: `Mapped[str]` is NOT NULL, `Mapped[str | None]` is nullable.
|
|
149
|
+
// The annotation is the declaration in this style, so it ranks above the
|
|
150
|
+
// 1.x-era default rather than below it.
|
|
151
|
+
const annotation = attribute.annotation;
|
|
152
|
+
if (annotation !== null && /\bMapped\s*\[/.test(annotation)) {
|
|
153
|
+
const inner = annotation.slice(annotation.indexOf("[") + 1, annotation.lastIndexOf("]"));
|
|
154
|
+
const nullable = /\bOptional\s*\[/.test(inner) || /\bNone\b/.test(inner);
|
|
155
|
+
return { name: attribute.name, nullable, nullableFrom: "annotation" };
|
|
156
|
+
}
|
|
157
|
+
// SQLAlchemy 1.x: a column is nullable unless it says otherwise. The opposite
|
|
158
|
+
// of Django's default, which is why neither is written once for both.
|
|
159
|
+
return { name: attribute.name, nullable: true, nullableFrom: "framework-default" };
|
|
160
|
+
}
|
|
161
|
+
/** The table a model binds to, where the source names one. */
|
|
162
|
+
function tableOf(attributes, meta, framework) {
|
|
163
|
+
const named = framework === "django" ? "db_table" : "__tablename__";
|
|
164
|
+
const source = framework === "django" ? meta : attributes;
|
|
165
|
+
for (const attribute of source) {
|
|
166
|
+
if (attribute.name === named && typeof attribute.literal === "string")
|
|
167
|
+
return attribute.literal;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The mapped shape of a class already established to belong to `framework`.
|
|
173
|
+
*
|
|
174
|
+
* Returns `null` where the class declares no column at all. That guard is what
|
|
175
|
+
* keeps a declarative base out of the graph as a model of its own: `class
|
|
176
|
+
* Base(DeclarativeBase): pass` inherits the provenance and maps nothing, and a
|
|
177
|
+
* `MODEL` with no fields is a claim about a table that does not exist.
|
|
178
|
+
*/
|
|
179
|
+
export function ormShapeOf(input) {
|
|
180
|
+
const fields = [];
|
|
181
|
+
const undecided = [];
|
|
182
|
+
for (const attribute of input.attributes) {
|
|
183
|
+
if (NOT_A_FIELD.has(attribute.name) || attribute.name.startsWith("__"))
|
|
184
|
+
continue;
|
|
185
|
+
if (!isColumnCall(attribute, input.framework))
|
|
186
|
+
continue;
|
|
187
|
+
const read = nullabilityOf(attribute, input.framework);
|
|
188
|
+
if ("undecided" in read)
|
|
189
|
+
undecided.push(attribute.name);
|
|
190
|
+
else
|
|
191
|
+
fields.push(read);
|
|
192
|
+
}
|
|
193
|
+
// **The column requirement is SQLAlchemy's alone, and the asymmetry is the
|
|
194
|
+
// frameworks', not this reader's.**
|
|
195
|
+
//
|
|
196
|
+
// SQLAlchemy's declarative base is a class the *project* writes — `class
|
|
197
|
+
// Base(DeclarativeBase): pass` — so it inherits the provenance that admits a
|
|
198
|
+
// model and maps nothing. Without this guard the base itself becomes a `MODEL`
|
|
199
|
+
// describing a table that does not exist.
|
|
200
|
+
//
|
|
201
|
+
// Django has no such class in the analysed set: `models.Model` is Django's own
|
|
202
|
+
// and is never a subclass of itself. Inheriting it IS the declaration, and
|
|
203
|
+
// `class Foo(models.Model): pass` is a complete model — Django gives it an
|
|
204
|
+
// implicit primary key. Requiring a column here cost 317 real models on
|
|
205
|
+
// django's own tree, which is how the asymmetry was found rather than reasoned
|
|
206
|
+
// about.
|
|
207
|
+
const table = tableOf(input.attributes, input.meta, input.framework);
|
|
208
|
+
if (input.framework === "sqlalchemy" &&
|
|
209
|
+
fields.length === 0 &&
|
|
210
|
+
undecided.length === 0 &&
|
|
211
|
+
table === null) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
framework: input.framework,
|
|
216
|
+
table,
|
|
217
|
+
fields: [...fields].sort((a, b) => a.name.localeCompare(b.name)),
|
|
218
|
+
undecided: [...undecided].sort(),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=orm.js.map
|