Skip to content
Merged
7 changes: 0 additions & 7 deletions internal/module/artifact/generate/webapistore.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ type FieldMetadata struct {
RelatedPath *string `json:"relatedPath,omitempty"`
RelatedStore *bool `json:"relatedStore,omitempty"`
Searchable *bool `json:"searchable,omitempty"`
RunAs *string `json:"runAs,omitempty"`
RelationModel *string `json:"relationModel,omitempty"`
RelationFilter *string `json:"relationFilter,omitempty"`
RelationModelParentField *string `json:"relationModelParentField,omitempty"`
Expand Down Expand Up @@ -123,12 +122,6 @@ func applyResolvedFieldContract(metadata *FieldMetadata, field *meta.IrField) {
metadata.Searchable = &searchable
}

if resolved.Resolved.RunAs.Value != nil {
metadata.RunAs = toStringPtr(*resolved.Resolved.RunAs.Value)
} else if resolved.Behavior.Compute != nil {
metadata.RunAs = toStringPtr(resolved.Behavior.Compute.RunAs)
}

if resolved.Structural.Translate != nil && *resolved.Structural.Translate {
t := true
metadata.Translate = &t
Expand Down
1 change: 0 additions & 1 deletion internal/module/artifact/generate/webapistore.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const {{.Model.Name}}FieldsMetadata = {
{{- if $field.RelatedPath}}relatedPath: '{{$field.RelatedPath}}',{{- end}}
{{- if ne $field.RelatedStore nil}}relatedStore: {{$field.RelatedStore}},{{- end}}
{{- if ne $field.Searchable nil}}searchable: {{$field.Searchable}},{{- end}}
{{- if $field.RunAs}}runAs: '{{$field.RunAs}}',{{- end}}
{{- if and $field.RelationModel (or (eq $field.FieldType "ManyToOne") (eq $field.FieldType "OneToMany") (eq $field.FieldType "ManyToMany") (eq $field.FieldType "ManyToOneRef") (eq $field.FieldType "ManyToManyRef"))}}relationModel: '{{$field.RelationModel}}',{{- end}}
{{- if $field.RelationFilter}}relationFilter: "{{$field.RelationFilter}}",{{- end}}
{{- if $field.RelationModelParentField}}relationModelParentField: '{{$field.RelationModelParentField}}',{{- end}}
Expand Down
19 changes: 14 additions & 5 deletions internal/module/artifact/generate/webapistore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package generator
import (
"context"
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strings"
Expand All @@ -22,7 +23,6 @@ func TestWebApiStoreGenerate(t *testing.T) {
selectionJSON := `[{"value":"allow","label":"Allow","labelText":{"key":"` + referenceKey + `","module":"demo","scope":"demo.status.allow","src":"Allow","kind":"literal"}}]`
round := "HALF_UP"
searchable := true
runAs := "system"
field := &meta.IrField{
BaseModel: meta.BaseModel{Id: sql.NullString{String: "field-1", Valid: true}},
Name: "Amount",
Expand Down Expand Up @@ -54,12 +54,11 @@ func TestWebApiStoreGenerate(t *testing.T) {
Related: &meta.IrFieldRelatedSpec{Path: "CurrencyId.Symbol", Store: true},
},
Behavior: meta.IrFieldBehaviorSpec{
Compute: &meta.IrFieldBehaviorComputeSpec{Method: "ComputeAmount", Deps: []string{"CurrencyId"}, Store: true, RunAs: "user"},
Compute: &meta.IrFieldBehaviorComputeSpec{Method: "ComputeAmount", Deps: []string{"CurrencyId"}, Store: true},
},
Migration: meta.IrFieldMigrationDecision{StorageKind: "column", ShouldCreateColumn: true, ResolvedColumnType: "NUMERIC(12,4)", ReasonCode: "LEGACY_COLUMN"},
}
resolvedSpec.Resolved.Searchable = meta.IrResolvedValue[*bool]{Value: &searchable, Source: "decorator"}
resolvedSpec.Resolved.RunAs = meta.IrResolvedValue[*string]{Value: &runAs, Source: "decorator"}
if err := field.SetResolvedSpec(resolvedSpec); err != nil {
t.Fatalf("set resolved spec: %v", err)
}
Expand All @@ -79,12 +78,19 @@ func TestWebApiStoreGenerate(t *testing.T) {
if metadata.RelatedPath == nil || *metadata.RelatedPath != "CurrencyId.Symbol" || metadata.Searchable == nil || !*metadata.Searchable {
t.Fatalf("expected related/searchable fields, got %#v", metadata)
}
if metadata.RunAs == nil || *metadata.RunAs != "system" || metadata.ShouldCreateColumn == nil || !*metadata.ShouldCreateColumn {
t.Fatalf("expected migration/runAs fields, got %#v", metadata)
if metadata.ShouldCreateColumn == nil || !*metadata.ShouldCreateColumn {
t.Fatalf("expected ShouldCreateColumn=true, got %#v", metadata)
}
if metadata.Translate != nil {
t.Fatalf("non-translate field must omit Translate, got %#v", metadata.Translate)
}
encodedMetadata, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("marshal field metadata: %v", err)
}
if strings.Contains(string(encodedMetadata), `"runAs"`) {
t.Fatalf("field metadata must omit removed runAs key, got %s", string(encodedMetadata))
}

relationFields, importModels := analyzeRelationFields(testApp().Models[1])
if len(relationFields) != 2 {
Expand Down Expand Up @@ -146,6 +152,9 @@ func TestWebApiStoreGenerate(t *testing.T) {
if !strings.Contains(string(storeContent), `string: "Amount"`) || !strings.Contains(string(storeContent), "stringText:") || !strings.Contains(string(storeContent), stringKey) {
t.Fatalf("expected field string/stringText in generated store content: %s", string(storeContent))
}
if strings.Contains(string(storeContent), "runAs:") || strings.Contains(string(storeContent), `"runAs"`) {
t.Fatalf("generated store must omit removed runAs key: %s", string(storeContent))
}
if _, err := os.Stat(filepath.Join(webStoreDir, "stores", "index.ts")); err != nil {
t.Fatalf("expected stores/index.ts: %v", err)
}
Expand Down
10 changes: 0 additions & 10 deletions internal/parser/backendtsparser/field_resolved.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,11 @@ func collectFieldBehaviorBindings(methods []*parser.MemberMethod) (map[string]*r
if v, ok := opts["searchable"].(bool); ok {
searchable = toBoolPtr(v)
}
runAs := ""
if v, ok := opts["runAs"].(string); ok {
runAs = strings.TrimSpace(v)
}
binding.compute = &meta.IrFieldBehaviorComputeSpec{
Method: method.Name,
Deps: deps,
Store: store,
Searchable: searchable,
RunAs: runAs,
}
case "SqlCompute":
if binding.sqlCompute != nil {
Expand Down Expand Up @@ -543,11 +538,6 @@ func buildFieldResolvedSpec(field *meta.IrField, binding *resolvedFieldBehaviorB
spec.Resolved.Searchable = meta.IrResolvedValue[*bool]{Value: spec.Behavior.Compute.Searchable, Source: "@Compute.searchable"}
}

if spec.Behavior.Compute != nil && strings.TrimSpace(spec.Behavior.Compute.RunAs) != "" {
runAs := strings.TrimSpace(spec.Behavior.Compute.RunAs)
spec.Resolved.RunAs = meta.IrResolvedValue[*string]{Value: &runAs, Source: "@Compute.runAs"}
}

columnType := resolveColumnType(fieldType)
if translate {
// Logical type stays char/varchar/text; physical storage is JSON/JSONB lang map.
Expand Down
55 changes: 52 additions & 3 deletions internal/parser/backendtsparser/parser_migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export default class Demo extends BaseModel {
@Field({ type: 'varchar', related: { path: 'PartnerId.Name', store: true, deps: ['PartnerId', 'PartnerId.Name'] } })
public PartnerName: string

@Compute<Demo>('PartnerName', { deps: ['Name'], store: false, searchable: true, runAs: 'sudo' })
@Compute<Demo>('PartnerName', { deps: ['Name'], store: false, searchable: true })
computePartnerName() {
return this.Name
}
Expand Down Expand Up @@ -290,8 +290,57 @@ export default class Demo extends BaseModel {
if partnerSpec.Migration.ShouldCreateColumn != false || partnerSpec.Migration.ReasonCode != "COMPUTE_STORE_FALSE" {
t.Fatalf("unexpected PartnerName migration decision: %+v", partnerSpec.Migration)
}
if partnerSpec.Resolved.RunAs.Value == nil || *partnerSpec.Resolved.RunAs.Value != "sudo" {
t.Fatalf("unexpected PartnerName runAs resolution: %+v", partnerSpec.Resolved.RunAs)
}

func TestTsParser_ParseModelOmitsLegacyComputeRunAs(t *testing.T) {
runtimeScope := newBackendParserTestScope()
module := &meta.IrModule{Path: "/virtual/modules/test", ApplicationStr: "test"}
p := NewTsParser(runtimeScope, module)

path := "/virtual/modules/test/service/demo_runas.ts"
content := `import { Model, Field, Compute } from '../../core/service';
import BaseModel from './base';

@Model('DemoRunAs')
export default class DemoRunAs extends BaseModel {
@Field({ type: 'varchar', size: 64 })
public Name: string

@Compute<DemoRunAs>('Name', { deps: ['Id'], store: false, runAs: 'sudo' })
computeName() {
return this.Name
}
}
`

r, err := p.Parse(map[string]string{}, path, content)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if r.Model == nil {
t.Fatal("expected parsed model")
}

var nameField *meta.IrField
for _, field := range r.Model.Fields {
if field.Name == "Name" {
nameField = field
break
}
}
if nameField == nil {
t.Fatal("expected Name field")
}

spec, err := nameField.GetResolvedSpec()
if err != nil || spec == nil {
t.Fatalf("parse Name resolved spec failed: %v spec=%v", err, spec)
}
if spec.Behavior.Compute == nil || spec.Behavior.Compute.Method != "computeName" {
t.Fatalf("unexpected compute behavior: %+v", spec.Behavior)
}
if strings.Contains(nameField.ResolvedSpec, `"runAs"`) {
t.Fatalf("resolved metadata must omit removed runAs contract, got %s", nameField.ResolvedSpec)
}
}

Expand Down
1 change: 1 addition & 0 deletions modules/core/service/api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
getReqMeta,
getUserId,
withContext,
withUser,
deleteReqStateKeysByPrefix,
invalidateJsCtxSymbolCache,
withBypassDepths,
Expand Down
1 change: 1 addition & 0 deletions modules/core/service/api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test('core/service/api entrypoint export surface stays limited to stable cross-m
'resolveValidationSummary',
'withContext',
'withI18nScope',
'withUser',
]);
});

Expand Down
1 change: 1 addition & 0 deletions modules/core/service/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
getReqMeta,
getUserId,
withContext,
withUser,
} from './context';

export {
Expand Down
8 changes: 3 additions & 5 deletions modules/core/service/orm/decorator/compute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ test('@Compute registers handler metadata with defaults', () => {
deps: ['Id', 'Id'],
store: false,
searchable: true,
runAs: 'sudo',
})
computeName() {
return undefined;
Expand All @@ -38,7 +37,6 @@ test('@Compute registers handler metadata with defaults', () => {
deps: ['Id'],
store: false,
searchable: true,
runAs: 'sudo',
});

resetModelMetadata(ComputeDecoratorModel as any);
Expand Down Expand Up @@ -142,16 +140,16 @@ test('@Compute rejects non-function descriptor value', () => {
}).toThrow('must decorate an instance method');
});

test('@Compute rejects invalid runAs', () => {
test('@Compute rejects author-facing runAs option', () => {
expect(() => {
class ComputeBadRunAsModel extends BaseModel {
@Compute<any>('Name', { deps: ['Id'], runAs: 'admin' as any })
@Compute<any>('Name', { deps: ['Id'], runAs: 'sudo' } as any)
computeName() {
return undefined;
}
}
return ComputeBadRunAsModel;
}).toThrow('runAs must be user or sudo');
}).toThrow('runAs is removed');
});

test('@Compute with store=false handles missing field entry gracefully', () => {
Expand Down
9 changes: 3 additions & 6 deletions modules/core/service/orm/decorator/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
import type BaseModel from '../model/model';
import { MetadataStorage } from '../metadata/storage';
import type { ComputeDep, ModelCtor } from '../metadata/field';
import type { ComputeRunAs } from '../metadata/compute';

export type ComputeOptions<TModel extends BaseModel = BaseModel> = {
deps: Array<ComputeDep<TModel>>;
store?: boolean;
searchable?: boolean;
runAs?: ComputeRunAs;
};

export function Compute<TModel extends BaseModel>(field: Extract<keyof TModel, string>, options: ComputeOptions<TModel>): MethodDecorator {
Expand All @@ -36,9 +34,9 @@ export function Compute<TModel extends BaseModel>(field: Extract<keyof TModel, s
throw new Error(`@Compute(${fieldName}) deps must be a non-empty array`);
}

const runAs = options?.runAs;
if (runAs != null && runAs !== 'user' && runAs !== 'sudo') {
throw new Error(`@Compute(${fieldName}) runAs must be user or sudo`);
const optionsRecord = options as Record<string, unknown> | undefined;
if (optionsRecord && Object.prototype.hasOwnProperty.call(optionsRecord, 'runAs')) {
throw new Error(`@Compute(${fieldName}) runAs is removed; call BaseModel.sudo / withUser inside the method body`);
}

const ctor = target.constructor as ModelCtor<BaseModel>;
Expand All @@ -62,7 +60,6 @@ export function Compute<TModel extends BaseModel>(field: Extract<keyof TModel, s
deps,
store: options?.store !== false,
searchable: typeof options?.searchable === 'boolean' ? options.searchable : undefined,
runAs,
});

MetadataStorage.instance.setModelMetadata(ctor, {
Expand Down
7 changes: 0 additions & 7 deletions modules/core/service/orm/metadata/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ import type { Operator, BaseQueryCondition, ExpressionWrapper } from '../reposit
import type { DialectName } from '../repository/repository_dialect';
import type { ObjectRecord } from '../../../utils/types';

/**
* Execution identity used when compute handlers run.
*/
export type ComputeRunAs = 'user' | 'sudo';

/**
* Operator type accepted by compute search handlers.
*/
Expand All @@ -32,7 +27,6 @@ export interface ComputeInverseContext<TModel, TValue> {
model: TModel;
value: TValue;
previousValue: TValue | undefined;
runAs: ComputeRunAs;
}

/**
Expand All @@ -48,7 +42,6 @@ export interface ComputeSearchContext<TDep extends string = string> {
op: ComputeOperator;
value: unknown;
dialect: DialectName;
runAs: ComputeRunAs;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions modules/core/service/orm/metadata/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import BaseModel from '../model/model';
import type { ExpressionWrapper, ExpressionBuilder, Expression } from 'kysely';
import Decimal, { DecimalRound } from '@/core/utils/decimal';
import type { ComputeRunAs } from './compute';
import type { TermReference } from '../../i18n';

type ObjectRecord = Record<string, unknown>;
Expand Down Expand Up @@ -398,7 +397,6 @@ export interface ColumnComputeSpec<T extends BaseModel = BaseModel, R = unknown>
deps: Array<ComputeDep<T>>;
store?: boolean;
searchable?: boolean;
runAs?: ComputeRunAs;
inverse?: string;
search?: string;
}
Expand Down
1 change: 0 additions & 1 deletion modules/core/service/orm/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export type {
} from './model';
export type { ValueType, ParamMetadata, ServiceMetadata } from './service';
export type {
ComputeRunAs,
ComputeOperator,
ComputeSearchDomain,
ComputeDeps,
Expand Down
1 change: 0 additions & 1 deletion modules/core/service/orm/metadata/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export interface ComputeHandlerMeta {
deps: string[];
store: boolean;
searchable?: boolean;
runAs?: 'user' | 'sudo';
}

/**
Expand Down
2 changes: 0 additions & 2 deletions modules/core/service/orm/metadata/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ test('metadata storage merges behavior handler maps and applies subclass overrid
deps: ['Amount', 'Tax'],
store: false,
searchable: true,
runAs: 'sudo',
},
],
]),
Expand All @@ -429,7 +428,6 @@ test('metadata storage merges behavior handler maps and applies subclass overrid
deps: ['Amount', 'Tax'],
store: false,
searchable: true,
runAs: 'sudo',
});
expect(merged.sqlComputeHandlers?.get('DisplayName')?.method).toBe('sqlDisplayNameParent');
expect(merged.searchHandlers?.get('VirtualName')?.method).toBe('searchVirtualNameParent');
Expand Down
1 change: 0 additions & 1 deletion modules/core/service/orm/metadata/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export class MetadataStorage {
deps,
store: typeof record.store === 'boolean' ? record.store : (existing?.store ?? true),
searchable: typeof record.searchable === 'boolean' ? record.searchable : existing?.searchable,
runAs: record.runAs === 'user' || record.runAs === 'sudo' ? record.runAs : existing?.runAs,
};
}

Expand Down
Loading