@rvoh/psychic 3.3.0 → 3.4.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/cjs/src/cli/index.js +11 -2
- package/dist/cjs/src/generate/helpers/parseAttribute.js +34 -4
- package/dist/esm/src/cli/index.js +11 -2
- package/dist/esm/src/generate/helpers/parseAttribute.js +34 -4
- package/dist/types/src/generate/helpers/parseAttribute.d.ts +9 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.
|
|
|
8
8
|
import generateResource from '../generate/resource.js';
|
|
9
9
|
import Watcher from '../watcher/Watcher.js';
|
|
10
10
|
const INDENT = ' ';
|
|
11
|
-
const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name) properties like this:
|
|
11
|
+
const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name, which may take an @alias suffix to rename the FK) properties like this:
|
|
12
12
|
${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
|
|
13
13
|
${INDENT}
|
|
14
14
|
${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
|
|
@@ -68,7 +68,16 @@ ${INDENT}
|
|
|
68
68
|
${INDENT} use the fully qualified model name (matching its path under src/app/models/):
|
|
69
69
|
${INDENT} User:belongs_to # creates user_id column + BelongsTo association
|
|
70
70
|
${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
|
|
71
|
-
${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
|
|
71
|
+
${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
|
|
72
|
+
${INDENT}
|
|
73
|
+
${INDENT} rename the association with Model@alias — the snake_case alias drives the FK column name AND the
|
|
74
|
+
${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
|
|
75
|
+
${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
|
|
76
|
+
${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
|
|
77
|
+
${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
|
|
78
|
+
${INDENT} # (strips the namespace from the property/association names while keeping
|
|
79
|
+
${INDENT} # the namespaced model reference intact)
|
|
80
|
+
${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`;
|
|
72
81
|
export default class PsychicCLI {
|
|
73
82
|
static provide(program, { initializePsychicApp, seedDb, }) {
|
|
74
83
|
DreamCLI.generateDreamCli(program, {
|
|
@@ -9,16 +9,46 @@ import { camelize } from '@rvoh/dream/utils';
|
|
|
9
9
|
* paramSafe column allowlist) share one canonical interpretation of the
|
|
10
10
|
* tokens — and one canonical casing (camelCase) for the resulting attribute
|
|
11
11
|
* name.
|
|
12
|
+
*
|
|
13
|
+
* Supports the `Model@alias:belongs_to` shorthand for renaming the FK
|
|
14
|
+
* association. When `@alias` is present, the camelized alias becomes
|
|
15
|
+
* `attributeName`; otherwise (legacy form `Model:belongs_to`) the
|
|
16
|
+
* attribute name is derived from the model's last namespace segment.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors Dream's CLI-side `parseAttribute` (kept as a parallel
|
|
19
|
+
* implementation rather than a shared import so neither package leaks an
|
|
20
|
+
* internal CLI helper through its public API surface).
|
|
12
21
|
*/
|
|
13
22
|
export default function parseAttribute(attribute) {
|
|
14
|
-
const
|
|
15
|
-
|
|
23
|
+
const segments = attribute.split(':');
|
|
24
|
+
const rawSegmentOne = segments[0];
|
|
25
|
+
const rawAttributeType = segments[1];
|
|
26
|
+
if (!rawSegmentOne || !rawAttributeType)
|
|
16
27
|
return null;
|
|
28
|
+
// Extract optional `@alias` from segment-1 (used by the belongs_to FK alias
|
|
29
|
+
// shorthand, e.g., `InternalUser@canceled_by:belongs_to`).
|
|
30
|
+
let rawAttributeName = rawSegmentOne;
|
|
31
|
+
let aliasName;
|
|
32
|
+
const atIndex = rawSegmentOne.indexOf('@');
|
|
33
|
+
if (atIndex !== -1) {
|
|
34
|
+
rawAttributeName = rawSegmentOne.slice(0, atIndex);
|
|
35
|
+
const rawAlias = rawSegmentOne.slice(atIndex + 1);
|
|
36
|
+
if (!rawAttributeName || !rawAlias)
|
|
37
|
+
return null;
|
|
38
|
+
aliasName = rawAlias;
|
|
39
|
+
}
|
|
40
|
+
// Pop trailing `optional` keyword from descriptors so it doesn't get
|
|
41
|
+
// mistaken for enum values or other positional descriptors.
|
|
42
|
+
const descriptors = segments.slice(2);
|
|
43
|
+
if (descriptors[descriptors.length - 1] === 'optional')
|
|
44
|
+
descriptors.pop();
|
|
45
|
+
const enumValues = descriptors[1];
|
|
17
46
|
const sanitizedAttrType = camelize(rawAttributeType)?.toLowerCase();
|
|
18
47
|
// Handle belongs_to relationships
|
|
19
48
|
if (sanitizedAttrType === 'belongsto') {
|
|
20
|
-
// For belongs_to relationships,
|
|
21
|
-
|
|
49
|
+
// For belongs_to relationships, prefer the explicit alias when present;
|
|
50
|
+
// otherwise convert "Ticketing/Ticket" → "ticket".
|
|
51
|
+
const attributeName = aliasName ? camelize(aliasName) : camelize(rawAttributeName.split('/').pop());
|
|
22
52
|
return { attributeName, attributeType: 'belongs_to', isArray: false, enumValues };
|
|
23
53
|
}
|
|
24
54
|
// Skip _type and _id columns, but not belongs_to relationships
|
|
@@ -8,7 +8,7 @@ import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.
|
|
|
8
8
|
import generateResource from '../generate/resource.js';
|
|
9
9
|
import Watcher from '../watcher/Watcher.js';
|
|
10
10
|
const INDENT = ' ';
|
|
11
|
-
const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name) properties like this:
|
|
11
|
+
const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name, which may take an @alias suffix to rename the FK) properties like this:
|
|
12
12
|
${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
|
|
13
13
|
${INDENT}
|
|
14
14
|
${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
|
|
@@ -68,7 +68,16 @@ ${INDENT}
|
|
|
68
68
|
${INDENT} use the fully qualified model name (matching its path under src/app/models/):
|
|
69
69
|
${INDENT} User:belongs_to # creates user_id column + BelongsTo association
|
|
70
70
|
${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
|
|
71
|
-
${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
|
|
71
|
+
${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
|
|
72
|
+
${INDENT}
|
|
73
|
+
${INDENT} rename the association with Model@alias — the snake_case alias drives the FK column name AND the
|
|
74
|
+
${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
|
|
75
|
+
${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
|
|
76
|
+
${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
|
|
77
|
+
${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
|
|
78
|
+
${INDENT} # (strips the namespace from the property/association names while keeping
|
|
79
|
+
${INDENT} # the namespaced model reference intact)
|
|
80
|
+
${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`;
|
|
72
81
|
export default class PsychicCLI {
|
|
73
82
|
static provide(program, { initializePsychicApp, seedDb, }) {
|
|
74
83
|
DreamCLI.generateDreamCli(program, {
|
|
@@ -9,16 +9,46 @@ import { camelize } from '@rvoh/dream/utils';
|
|
|
9
9
|
* paramSafe column allowlist) share one canonical interpretation of the
|
|
10
10
|
* tokens — and one canonical casing (camelCase) for the resulting attribute
|
|
11
11
|
* name.
|
|
12
|
+
*
|
|
13
|
+
* Supports the `Model@alias:belongs_to` shorthand for renaming the FK
|
|
14
|
+
* association. When `@alias` is present, the camelized alias becomes
|
|
15
|
+
* `attributeName`; otherwise (legacy form `Model:belongs_to`) the
|
|
16
|
+
* attribute name is derived from the model's last namespace segment.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors Dream's CLI-side `parseAttribute` (kept as a parallel
|
|
19
|
+
* implementation rather than a shared import so neither package leaks an
|
|
20
|
+
* internal CLI helper through its public API surface).
|
|
12
21
|
*/
|
|
13
22
|
export default function parseAttribute(attribute) {
|
|
14
|
-
const
|
|
15
|
-
|
|
23
|
+
const segments = attribute.split(':');
|
|
24
|
+
const rawSegmentOne = segments[0];
|
|
25
|
+
const rawAttributeType = segments[1];
|
|
26
|
+
if (!rawSegmentOne || !rawAttributeType)
|
|
16
27
|
return null;
|
|
28
|
+
// Extract optional `@alias` from segment-1 (used by the belongs_to FK alias
|
|
29
|
+
// shorthand, e.g., `InternalUser@canceled_by:belongs_to`).
|
|
30
|
+
let rawAttributeName = rawSegmentOne;
|
|
31
|
+
let aliasName;
|
|
32
|
+
const atIndex = rawSegmentOne.indexOf('@');
|
|
33
|
+
if (atIndex !== -1) {
|
|
34
|
+
rawAttributeName = rawSegmentOne.slice(0, atIndex);
|
|
35
|
+
const rawAlias = rawSegmentOne.slice(atIndex + 1);
|
|
36
|
+
if (!rawAttributeName || !rawAlias)
|
|
37
|
+
return null;
|
|
38
|
+
aliasName = rawAlias;
|
|
39
|
+
}
|
|
40
|
+
// Pop trailing `optional` keyword from descriptors so it doesn't get
|
|
41
|
+
// mistaken for enum values or other positional descriptors.
|
|
42
|
+
const descriptors = segments.slice(2);
|
|
43
|
+
if (descriptors[descriptors.length - 1] === 'optional')
|
|
44
|
+
descriptors.pop();
|
|
45
|
+
const enumValues = descriptors[1];
|
|
17
46
|
const sanitizedAttrType = camelize(rawAttributeType)?.toLowerCase();
|
|
18
47
|
// Handle belongs_to relationships
|
|
19
48
|
if (sanitizedAttrType === 'belongsto') {
|
|
20
|
-
// For belongs_to relationships,
|
|
21
|
-
|
|
49
|
+
// For belongs_to relationships, prefer the explicit alias when present;
|
|
50
|
+
// otherwise convert "Ticketing/Ticket" → "ticket".
|
|
51
|
+
const attributeName = aliasName ? camelize(aliasName) : camelize(rawAttributeName.split('/').pop());
|
|
22
52
|
return { attributeName, attributeType: 'belongs_to', isArray: false, enumValues };
|
|
23
53
|
}
|
|
24
54
|
// Skip _type and _id columns, but not belongs_to relationships
|
|
@@ -14,5 +14,14 @@ export interface ParsedAttribute {
|
|
|
14
14
|
* paramSafe column allowlist) share one canonical interpretation of the
|
|
15
15
|
* tokens — and one canonical casing (camelCase) for the resulting attribute
|
|
16
16
|
* name.
|
|
17
|
+
*
|
|
18
|
+
* Supports the `Model@alias:belongs_to` shorthand for renaming the FK
|
|
19
|
+
* association. When `@alias` is present, the camelized alias becomes
|
|
20
|
+
* `attributeName`; otherwise (legacy form `Model:belongs_to`) the
|
|
21
|
+
* attribute name is derived from the model's last namespace segment.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors Dream's CLI-side `parseAttribute` (kept as a parallel
|
|
24
|
+
* implementation rather than a shared import so neither package leaks an
|
|
25
|
+
* internal CLI helper through its public API surface).
|
|
17
26
|
*/
|
|
18
27
|
export default function parseAttribute(attribute: string): ParsedAttribute | null;
|