@apso/cli 0.37.1 → 0.38.4
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/lib/apsorc-parser.d.ts +18 -0
- package/dist/lib/apsorc-parser.js +68 -1
- package/dist/lib/base-command.d.ts +5 -0
- package/dist/lib/base-command.js +15 -1
- package/dist/lib/config/types.d.ts +4 -0
- package/dist/lib/telemetry/telemetry.js +43 -11
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,24 @@ type ParsedApsorc = {
|
|
|
51
51
|
coAuthor?: boolean;
|
|
52
52
|
};
|
|
53
53
|
export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
54
|
+
/**
|
|
55
|
+
* A name in .apsorc that would generate code that does not compile. `code`
|
|
56
|
+
* matches the apso-client-v2 schema validator (src/lib/ai/validation.ts).
|
|
57
|
+
*/
|
|
58
|
+
export declare class ApsorcNamingError extends Error {
|
|
59
|
+
code: string;
|
|
60
|
+
entity: string;
|
|
61
|
+
field?: string | undefined;
|
|
62
|
+
constructor(message: string, code: string, entity: string, field?: string | undefined);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Throws an ApsorcNamingError when .apsorc names would generate code that does
|
|
66
|
+
* not compile, e.g. a text field `notes` on Contact plus Note ManyToOne
|
|
67
|
+
* Contact, whose inverse side is `notes: Note[]` on Contact (TS2300 Duplicate
|
|
68
|
+
* identifier). The client schema validator enforces the same rules; both repos
|
|
69
|
+
* run test/fixtures/codegen-naming-cases.json, so change them together.
|
|
70
|
+
*/
|
|
71
|
+
export declare const assertCodegenSafeNames: (entities: Entity[], relationshipMap: RelationshipMap) => void;
|
|
54
72
|
export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
55
73
|
export declare const parseApsorc: () => ParsedApsorc;
|
|
56
74
|
/**
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findConfigPath = exports.parseApsorc = exports.parseApsorcV2 = exports.parseApsorcV1 = exports.ApiType = void 0;
|
|
3
|
+
exports.findConfigPath = exports.parseApsorc = exports.parseApsorcV2 = exports.assertCodegenSafeNames = exports.ApsorcNamingError = exports.parseApsorcV1 = exports.ApiType = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
7
|
const rc_1 = tslib_1.__importDefault(require("rc"));
|
|
8
8
|
const relationships_1 = require("./utils/relationships");
|
|
9
9
|
const perf_hooks_1 = require("perf_hooks");
|
|
10
|
+
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
|
|
11
|
+
const casing_1 = require("./utils/casing");
|
|
10
12
|
var ApiType;
|
|
11
13
|
(function (ApiType) {
|
|
12
14
|
ApiType["Graphql"] = "graphql";
|
|
@@ -21,9 +23,74 @@ const parseApsorcV1 = (apsorc) => {
|
|
|
21
23
|
return { entities, relationshipMap };
|
|
22
24
|
};
|
|
23
25
|
exports.parseApsorcV1 = parseApsorcV1;
|
|
26
|
+
/** Valid in TypeScript, Python and Go, so every target language compiles it. */
|
|
27
|
+
const IDENTIFIER = /^[A-Z_a-z]\w*$/;
|
|
28
|
+
/**
|
|
29
|
+
* A name in .apsorc that would generate code that does not compile. `code`
|
|
30
|
+
* matches the apso-client-v2 schema validator (src/lib/ai/validation.ts).
|
|
31
|
+
*/
|
|
32
|
+
class ApsorcNamingError extends Error {
|
|
33
|
+
constructor(message, code, entity, field) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.entity = entity;
|
|
37
|
+
this.field = field;
|
|
38
|
+
this.name = "ApsorcNamingError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.ApsorcNamingError = ApsorcNamingError;
|
|
42
|
+
/**
|
|
43
|
+
* Throws an ApsorcNamingError when .apsorc names would generate code that does
|
|
44
|
+
* not compile, e.g. a text field `notes` on Contact plus Note ManyToOne
|
|
45
|
+
* Contact, whose inverse side is `notes: Note[]` on Contact (TS2300 Duplicate
|
|
46
|
+
* identifier). The client schema validator enforces the same rules; both repos
|
|
47
|
+
* run test/fixtures/codegen-naming-cases.json, so change them together.
|
|
48
|
+
*/
|
|
49
|
+
const assertCodegenSafeNames = (entities, relationshipMap) => {
|
|
50
|
+
const seenEntities = new Set();
|
|
51
|
+
for (const entity of entities) {
|
|
52
|
+
if (seenEntities.has(entity.name)) {
|
|
53
|
+
throw new ApsorcNamingError(`Entity "${entity.name}" is defined more than once. Merge or rename one of them in .apsorc.`, "DUPLICATE_ENTITY_NAME", entity.name);
|
|
54
|
+
}
|
|
55
|
+
seenEntities.add(entity.name);
|
|
56
|
+
if (!IDENTIFIER.test(entity.name)) {
|
|
57
|
+
throw new ApsorcNamingError(`Entity "${entity.name}" is not a valid name. Use letters, digits and underscores, starting with a letter.`, "INVALID_ENTITY_NAME", entity.name);
|
|
58
|
+
}
|
|
59
|
+
const fieldNames = new Set();
|
|
60
|
+
for (const { name } of entity.fields || []) {
|
|
61
|
+
if (!IDENTIFIER.test(name)) {
|
|
62
|
+
throw new ApsorcNamingError(`Field "${name}" on entity "${entity.name}" is not a valid name. Use letters, digits and underscores.`, "INVALID_FIELD_NAME", entity.name, name);
|
|
63
|
+
}
|
|
64
|
+
if (fieldNames.has(name)) {
|
|
65
|
+
throw new ApsorcNamingError(`Entity "${entity.name}" has more than one field named "${name}". Remove or rename one in .apsorc.`, "DUPLICATE_FIELD_NAME", entity.name, name);
|
|
66
|
+
}
|
|
67
|
+
fieldNames.add(name);
|
|
68
|
+
}
|
|
69
|
+
// Same property names and dedupe key as getRelationshipForTemplate + entity.eta.
|
|
70
|
+
const generated = new Map();
|
|
71
|
+
for (const rel of relationshipMap[entity.name] || []) {
|
|
72
|
+
const base = rel.referenceName || rel.name;
|
|
73
|
+
const property = (0, casing_1.camelCase)(rel.type === "OneToMany" || rel.type === "ManyToMany"
|
|
74
|
+
? (0, pluralize_1.default)(base)
|
|
75
|
+
: base);
|
|
76
|
+
const key = `${rel.name}:${rel.referenceName || "default"}`;
|
|
77
|
+
const desc = `${rel.type} relationship to "${rel.name}"`;
|
|
78
|
+
if (fieldNames.has(property)) {
|
|
79
|
+
throw new ApsorcNamingError(`Entity "${entity.name}" has a field "${property}" with the same name as the property generated by its ${desc}. Rename the field in .apsorc.`, "FIELD_RELATIONSHIP_COLLISION", entity.name, property);
|
|
80
|
+
}
|
|
81
|
+
const existing = generated.get(property);
|
|
82
|
+
if (existing && existing.key !== key) {
|
|
83
|
+
throw new ApsorcNamingError(`Entity "${entity.name}" gets the property "${property}" from both its ${existing.rel} and its ${desc}. Give one of them a different "to_name" in .apsorc.`, "RELATIONSHIP_PROPERTY_CLASH", entity.name, property);
|
|
84
|
+
}
|
|
85
|
+
generated.set(property, { key, rel: desc });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.assertCodegenSafeNames = assertCodegenSafeNames;
|
|
24
90
|
const parseApsorcV2 = (apsorc) => {
|
|
25
91
|
const { entities, relationships: apsoRelationships } = apsorc;
|
|
26
92
|
const relationshipMap = (0, relationships_1.parseRelationships)(apsoRelationships);
|
|
93
|
+
(0, exports.assertCodegenSafeNames)(entities, relationshipMap);
|
|
27
94
|
return { entities, relationshipMap };
|
|
28
95
|
};
|
|
29
96
|
exports.parseApsorcV2 = parseApsorcV2;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
+
/** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
|
|
3
|
+
export declare function isUserError(err: unknown): boolean;
|
|
2
4
|
export default abstract class BaseCommand extends Command {
|
|
3
5
|
/**
|
|
4
6
|
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
@@ -7,6 +9,9 @@ export default abstract class BaseCommand extends Command {
|
|
|
7
9
|
catch(err: Error & {
|
|
8
10
|
exitCode?: number;
|
|
9
11
|
code?: string;
|
|
12
|
+
oclif?: {
|
|
13
|
+
exit?: number;
|
|
14
|
+
};
|
|
10
15
|
}): Promise<any>;
|
|
11
16
|
runCommand(command: string, args: string[], silent?: boolean): Promise<void>;
|
|
12
17
|
runNpmCommand(args: string[], silent?: boolean): Promise<void>;
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isUserError = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const child_process_1 = require("child_process");
|
|
6
7
|
const os_1 = tslib_1.__importDefault(require("os"));
|
|
7
8
|
const telemetry_1 = require("./telemetry/telemetry");
|
|
9
|
+
/** True for errors oclif raised deliberately (this.error, this.exit, parse errors). */
|
|
10
|
+
function isUserError(err) {
|
|
11
|
+
return Boolean(err && typeof err === "object" && "oclif" in err);
|
|
12
|
+
}
|
|
13
|
+
exports.isUserError = isUserError;
|
|
8
14
|
class BaseCommand extends core_1.Command {
|
|
9
15
|
/**
|
|
10
16
|
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
@@ -12,12 +18,20 @@ class BaseCommand extends core_1.Command {
|
|
|
12
18
|
*/
|
|
13
19
|
async catch(err) {
|
|
14
20
|
try {
|
|
15
|
-
|
|
21
|
+
// oclif marks errors it raised on purpose (this.error, this.exit, flag
|
|
22
|
+
// and arg parse failures) with an `oclif` property. Those are messages
|
|
23
|
+
// we chose to show the user, e.g. "No entity named X", not crashes, so
|
|
24
|
+
// they go to PostHog only. Everything else is a real bug for Sentry.
|
|
25
|
+
const userError = isUserError(err);
|
|
26
|
+
if (!userError) {
|
|
27
|
+
(0, telemetry_1.captureException)(err, { command: this.id });
|
|
28
|
+
}
|
|
16
29
|
(0, telemetry_1.track)("cli_command_failed", {
|
|
17
30
|
command: this.id,
|
|
18
31
|
duration_ms: (0, telemetry_1.elapsedMs)(),
|
|
19
32
|
error: err === null || err === void 0 ? void 0 : err.message,
|
|
20
33
|
error_code: err === null || err === void 0 ? void 0 : err.code,
|
|
34
|
+
user_error: userError,
|
|
21
35
|
});
|
|
22
36
|
await (0, telemetry_1.shutdownTelemetry)();
|
|
23
37
|
}
|
|
@@ -133,6 +133,10 @@ export interface GlobalConfigFile {
|
|
|
133
133
|
* Whether the first-run telemetry transparency notice has been shown.
|
|
134
134
|
*/
|
|
135
135
|
telemetryNoticeShown: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* User id this install's anonymous telemetry id was last aliased to.
|
|
138
|
+
*/
|
|
139
|
+
telemetryAliasedUserId?: string;
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
142
|
* Project link stored in .apso/link.json
|
|
@@ -45,6 +45,9 @@ let enabled = false;
|
|
|
45
45
|
let sentryOn = false;
|
|
46
46
|
let authenticatedUser = false;
|
|
47
47
|
let distinctId = "cli-anonymous";
|
|
48
|
+
let workspaceId;
|
|
49
|
+
let workspaceSlug;
|
|
50
|
+
let serviceId;
|
|
48
51
|
let environment = "production";
|
|
49
52
|
let startedAt = Date.now();
|
|
50
53
|
let noticePending = false;
|
|
@@ -85,7 +88,7 @@ function getOrCreateInstallId(currentInstallId) {
|
|
|
85
88
|
* when disabled — it just no-ops everything downstream.
|
|
86
89
|
*/
|
|
87
90
|
function initTelemetry() {
|
|
88
|
-
var _a, _b
|
|
91
|
+
var _a, _b;
|
|
89
92
|
try {
|
|
90
93
|
startedAt = Date.now();
|
|
91
94
|
const cfg = config_1.globalConfig.read();
|
|
@@ -101,14 +104,33 @@ function initTelemetry() {
|
|
|
101
104
|
noticePending = !cfg.telemetryNoticeShown;
|
|
102
105
|
const creds = config_1.credentials.read();
|
|
103
106
|
authenticatedUser = Boolean((_a = creds === null || creds === void 0 ? void 0 : creds.user) === null || _a === void 0 ? void 0 : _a.id);
|
|
104
|
-
|
|
107
|
+
const installId = getOrCreateInstallId(cfg.installId);
|
|
108
|
+
// Logged in: the Apso user id (the same distinct id the web app uses), so
|
|
109
|
+
// CLI and app activity land on one person. No email or name is sent.
|
|
110
|
+
distinctId = ((_b = creds === null || creds === void 0 ? void 0 : creds.user) === null || _b === void 0 ? void 0 : _b.id) ? String(creds.user.id) : installId;
|
|
111
|
+
// Workspace context: the linked project (.apso/link.json) wins, else the
|
|
112
|
+
// workspace chosen with `apso use`.
|
|
113
|
+
let link = null;
|
|
114
|
+
try {
|
|
115
|
+
link = config_1.projectLink.read();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
link = null;
|
|
119
|
+
}
|
|
120
|
+
workspaceId = (link === null || link === void 0 ? void 0 : link.workspaceId) || cfg.activeWorkspaceId || undefined;
|
|
121
|
+
workspaceSlug = (link === null || link === void 0 ? void 0 : link.workspaceSlug) || cfg.activeWorkspaceSlug || undefined;
|
|
122
|
+
serviceId = (link === null || link === void 0 ? void 0 : link.serviceId) || undefined;
|
|
105
123
|
posthog = new posthog_node_1.PostHog(environment === "staging" ? POSTHOG_KEY_STAGING : POSTHOG_KEY_PROD, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
// First run after login: merge this install's earlier anonymous events
|
|
125
|
+
// into the user, once per user.
|
|
126
|
+
if (authenticatedUser && installId !== distinctId && cfg.telemetryAliasedUserId !== distinctId) {
|
|
127
|
+
posthog.alias({ distinctId, alias: installId });
|
|
128
|
+
try {
|
|
129
|
+
config_1.globalConfig.write({ telemetryAliasedUserId: distinctId });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore — worst case we alias again next run
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
const dsn = process.env.APSO_CLI_SENTRY_DSN || SENTRY_DSN_DEFAULT;
|
|
114
136
|
if (dsn) {
|
|
@@ -118,8 +140,12 @@ function initTelemetry() {
|
|
|
118
140
|
release: `apso-cli@${cliVersion}`,
|
|
119
141
|
tracesSampleRate: 0,
|
|
120
142
|
});
|
|
121
|
-
Sentry.setUser({ id: distinctId
|
|
143
|
+
Sentry.setUser({ id: distinctId });
|
|
122
144
|
Sentry.setTag("cli_version", cliVersion);
|
|
145
|
+
if (workspaceId)
|
|
146
|
+
Sentry.setTag("workspace_id", workspaceId);
|
|
147
|
+
if (serviceId)
|
|
148
|
+
Sentry.setTag("service_id", serviceId);
|
|
123
149
|
sentryOn = true;
|
|
124
150
|
}
|
|
125
151
|
}
|
|
@@ -136,14 +162,18 @@ function commonProps() {
|
|
|
136
162
|
arch: os_1.default.arch(),
|
|
137
163
|
node_version: process.version,
|
|
138
164
|
authenticated: authenticatedUser,
|
|
165
|
+
...(workspaceId ? { workspace_id: workspaceId } : {}),
|
|
166
|
+
...(workspaceSlug ? { workspace_slug: workspaceSlug } : {}),
|
|
167
|
+
...(serviceId ? { service_id: serviceId } : {}),
|
|
139
168
|
};
|
|
140
169
|
}
|
|
141
170
|
/**
|
|
142
171
|
* The one-time transparency notice text (issue #96). Printed to stderr by the
|
|
143
172
|
* init hook so it never pollutes stdout / piped output.
|
|
144
173
|
*/
|
|
145
|
-
exports.FIRST_RUN_NOTICE = "Apso collects
|
|
146
|
-
"
|
|
174
|
+
exports.FIRST_RUN_NOTICE = "Apso collects usage data (command name, CLI version, OS) to improve the tool. When you're\n" +
|
|
175
|
+
"logged in, it's tied to your Apso account id and workspace id. No email, code, schema, or\n" +
|
|
176
|
+
"file contents are collected.\n" +
|
|
147
177
|
"Opt out any time: apso config set telemetry off (or APSO_TELEMETRY=0, or DO_NOT_TRACK=1)";
|
|
148
178
|
/**
|
|
149
179
|
* True once per install: telemetry is enabled and the notice hasn't been shown
|
|
@@ -173,6 +203,8 @@ function track(event, properties = {}) {
|
|
|
173
203
|
distinctId,
|
|
174
204
|
event,
|
|
175
205
|
properties: { ...commonProps(), ...properties },
|
|
206
|
+
// PostHog group analytics: roll activity up per workspace.
|
|
207
|
+
...(workspaceId ? { groups: { workspace: workspaceId } } : {}),
|
|
176
208
|
});
|
|
177
209
|
}
|
|
178
210
|
catch {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.38.4",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@biomejs/biome": "^1.9.4",
|
package/oclif.manifest.json
CHANGED