From 0da189a57be1e9778d84b0241a5d9ce29ee30a0f Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Tue, 17 Mar 2026 20:38:31 +0100 Subject: [PATCH 01/19] feat: abapGit roundtrip - export, deploy, and structure support Add end-to-end abapGit roundtrip capability: export ABAP objects from SAP to abapGit-format files, then deploy them back. Includes new DDIC object handlers (DOMA, DTEL, TABL, TTYP), structure (TABL/DS) support, and the roundtrip/activate CLI commands. Key changes: ADK (adk): - Registry: add resolveType() with full-type-first fallback for subtypes - Registry: add getMainType() helper and getObjectUri() using resolveType - Model: add crudContract-based load/save, lock/unlock, activate lifecycle - ObjectSet: add deploy() with two-phase save+activate, BulkSaveResult - TABL: add AdkTable and AdkStructure models with source support - DOMA/DTEL/TTYP: add ADK models with CRUD contract endpoints - DEVC: preserve full ADT type (TABL/DS) instead of stripping subtypes - CLAS/INTF/PROG/FUGR: add save lifecycle (savePendingSources, checkUnchanged) - Export getMainType, resolveType, getObjectUri from index abapGit plugin (adt-plugin-abapgit): - XSD: reorder DD02V/DD03P fields to match abapGit output ordering - XSD: add DD04V fields (DDLANGUAGE, DDTEXT, DOMNAME, etc.) - Codegen: regenerate all schemas and types from updated XSDs - Handlers: add DOMA handler with domain-specific value serialization - Handlers: add DTEL handler with data element metadata serialization - Handlers: add TABL handler (tables) and structure handler (TABL/DS) - Handlers: add TTYP handler for table types - Handlers: add cds-to-abapgit.ts converter for CDS-sourced tables - Base: use getMainType() for file extension (TABL/DS -> .tabl) - Deserializer: use payload.type for correct subtype resolution - Tests: add DTEL and TABL handler tests CLI (adt-cli): - Commands: add check, unlock, import object commands - Import service: add single-object import by name - Import service: preserve full ADT type in object type filter - Plugin loader: support format plugins with import/export capabilities Export plugin (adt-export): - Commands: add roundtrip command (export -> deploy cycle) - Commands: add activate command for bulk activation - Export: enhanced FileTree with directory walking and file operations - Package.json: add @abapify/adk dependency Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- adt.config.ts | 4 + git_modules/abapgit-examples | 2 +- packages/acds/eslint.config.js | 3 + packages/acds/package.json | 31 + packages/acds/src/ast.ts | 218 +++++++ packages/acds/src/errors.ts | 33 + packages/acds/src/index.ts | 101 ++++ packages/acds/src/parser.test.ts | 481 +++++++++++++++ packages/acds/src/parser.ts | 333 +++++++++++ packages/acds/src/tokens.ts | 234 ++++++++ packages/acds/src/visitor.ts | 277 +++++++++ packages/acds/tsconfig.json | 8 + packages/acds/tsdown.config.ts | 7 + packages/acds/vitest.config.ts | 8 + packages/adk/src/base/fetch-utils.ts | 26 + packages/adk/src/base/model.ts | 20 +- packages/adk/src/base/object-set.ts | 17 +- packages/adk/src/base/registry.ts | 68 ++- packages/adk/src/index.ts | 4 + .../adk/src/objects/ddic/doma/doma.model.ts | 2 +- .../adk/src/objects/ddic/dtel/dtel.model.ts | 4 +- .../adk/src/objects/ddic/tabl/tabl.model.ts | 55 +- .../adk/src/objects/ddic/ttyp/ttyp.model.ts | 4 +- .../src/objects/repository/clas/clas.model.ts | 2 +- .../src/objects/repository/devc/devc.model.ts | 26 +- .../src/objects/repository/fugr/fugr.model.ts | 4 +- .../src/objects/repository/intf/intf.model.ts | 4 +- .../src/objects/repository/prog/prog.model.ts | 4 +- packages/adt-cli/src/lib/cli.ts | 10 + packages/adt-cli/src/lib/commands/check.ts | 437 ++++++++++++++ .../adt-cli/src/lib/commands/import/object.ts | 84 +++ packages/adt-cli/src/lib/commands/index.ts | 3 + packages/adt-cli/src/lib/commands/unlock.ts | 165 +++++ packages/adt-cli/src/lib/plugin-loader.ts | 7 +- .../src/lib/services/import/service.ts | 195 +++++- packages/adt-cli/src/lib/utils/object-uri.ts | 117 ++-- packages/adt-export/package.json | 9 +- packages/adt-export/src/commands/activate.ts | 292 +++++++++ packages/adt-export/src/commands/export.ts | 124 +++- packages/adt-export/src/commands/roundtrip.ts | 565 ++++++++++++++++++ packages/adt-export/src/index.ts | 9 +- packages/adt-export/src/utils/filetree.ts | 94 ++- packages/adt-export/tsdown.config.ts | 7 +- packages/adt-mcp/tsconfig.json | 10 +- packages/adt-plugin-abapgit/package.json | 1 + packages/adt-plugin-abapgit/project.json | 2 +- .../src/lib/deserializer.ts | 56 +- .../src/lib/handlers/base.ts | 28 +- .../src/lib/handlers/cds-to-abapgit.ts | 310 ++++++++++ .../src/lib/handlers/objects/clas.ts | 2 +- .../src/lib/handlers/objects/devc.ts | 2 +- .../src/lib/handlers/objects/doma.ts | 79 ++- .../src/lib/handlers/objects/dtel.ts | 213 ++++++- .../src/lib/handlers/objects/fugr.ts | 2 +- .../src/lib/handlers/objects/index.ts | 2 +- .../src/lib/handlers/objects/intf.ts | 2 +- .../src/lib/handlers/objects/prog.ts | 2 +- .../src/lib/handlers/objects/tabl.ts | 174 +++++- .../src/lib/handlers/objects/ttyp.ts | 15 +- .../src/schemas/generated/schemas/clas.ts | 7 +- .../src/schemas/generated/schemas/devc.ts | 7 +- .../src/schemas/generated/schemas/doma.ts | 7 +- .../src/schemas/generated/schemas/dtel.ts | 47 +- .../src/schemas/generated/schemas/fugr.ts | 3 +- .../src/schemas/generated/schemas/intf.ts | 7 +- .../src/schemas/generated/schemas/prog.ts | 3 +- .../src/schemas/generated/schemas/tabl.ts | 36 +- .../src/schemas/generated/schemas/ttyp.ts | 7 +- .../src/schemas/generated/types/clas.ts | 24 + .../src/schemas/generated/types/devc.ts | 7 + .../src/schemas/generated/types/doma.ts | 29 + .../src/schemas/generated/types/dtel.ts | 46 +- .../src/schemas/generated/types/intf.ts | 13 + .../src/schemas/generated/types/tabl.ts | 64 +- .../src/schemas/generated/types/ttyp.ts | 20 + .../tests/handlers/dtel-e2e.test.ts | 267 +++++++++ .../tests/handlers/dtel.test.ts | 480 +++++++++++++++ .../tests/handlers/tabl.test.ts | 372 ++++++++++++ packages/adt-plugin-abapgit/tsconfig.lib.json | 3 + packages/adt-plugin-abapgit/xsd/asx.xsd | 5 +- .../adt-plugin-abapgit/xsd/types/dd02v.xsd | 6 +- .../adt-plugin-abapgit/xsd/types/dd03p.xsd | 5 +- .../adt-plugin-abapgit/xsd/types/dd04v.xsd | 14 +- packages/adt-plugin/src/cli-types.ts | 6 + tsconfig.json | 9 + 85 files changed, 6220 insertions(+), 272 deletions(-) create mode 100644 packages/acds/eslint.config.js create mode 100644 packages/acds/package.json create mode 100644 packages/acds/src/ast.ts create mode 100644 packages/acds/src/errors.ts create mode 100644 packages/acds/src/index.ts create mode 100644 packages/acds/src/parser.test.ts create mode 100644 packages/acds/src/parser.ts create mode 100644 packages/acds/src/tokens.ts create mode 100644 packages/acds/src/visitor.ts create mode 100644 packages/acds/tsconfig.json create mode 100644 packages/acds/tsdown.config.ts create mode 100644 packages/acds/vitest.config.ts create mode 100644 packages/adk/src/base/fetch-utils.ts create mode 100644 packages/adt-cli/src/lib/commands/check.ts create mode 100644 packages/adt-cli/src/lib/commands/import/object.ts create mode 100644 packages/adt-cli/src/lib/commands/unlock.ts create mode 100644 packages/adt-export/src/commands/activate.ts create mode 100644 packages/adt-export/src/commands/roundtrip.ts create mode 100644 packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts create mode 100644 packages/adt-plugin-abapgit/tests/handlers/dtel-e2e.test.ts create mode 100644 packages/adt-plugin-abapgit/tests/handlers/dtel.test.ts create mode 100644 packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts diff --git a/adt.config.ts b/adt.config.ts index 08f429cd..f760b2c2 100644 --- a/adt.config.ts +++ b/adt.config.ts @@ -19,5 +19,9 @@ export default { '@abapify/adt-aunit/commands/aunit', // Export plugin - deploy local files to SAP (aliased as 'deploy') '@abapify/adt-export/commands/export', + // Roundtrip test - deploy, reimport, compare + '@abapify/adt-export/commands/roundtrip', + // Activate - bulk activate inactive objects + '@abapify/adt-export/commands/activate', ], } as AdtConfig; diff --git a/git_modules/abapgit-examples b/git_modules/abapgit-examples index 74cd5bc8..3161cc18 160000 --- a/git_modules/abapgit-examples +++ b/git_modules/abapgit-examples @@ -1 +1 @@ -Subproject commit 74cd5bc854a8b4c83397881fd0ae66813b099b56 +Subproject commit 3161cc18c1cea9004ddb1cf49a5faad4c3539427 diff --git a/packages/acds/eslint.config.js b/packages/acds/eslint.config.js new file mode 100644 index 00000000..b7f62772 --- /dev/null +++ b/packages/acds/eslint.config.js @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [...baseConfig]; diff --git a/packages/acds/package.json b/packages/acds/package.json new file mode 100644 index 00000000..cb7a8109 --- /dev/null +++ b/packages/acds/package.json @@ -0,0 +1,31 @@ +{ + "name": "@abapify/acds", + "publishConfig": { + "access": "public" + }, + "version": "0.0.1", + "description": "ABAP CDS source parser — tokenizer, parser, and AST for all DDL-based ABAP source types", + "type": "module", + "types": "./dist/index.d.mts", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "dependencies": { + "chevrotain": "^11.0.0" + }, + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "abap", + "cds", + "parser", + "ddl", + "sap", + "adt" + ], + "author": "abapify", + "license": "MIT" +} diff --git a/packages/acds/src/ast.ts b/packages/acds/src/ast.ts new file mode 100644 index 00000000..f1f7535b --- /dev/null +++ b/packages/acds/src/ast.ts @@ -0,0 +1,218 @@ +/** + * AST Node Types for ABAP CDS source + * + * Phase 1: TABL, Structure, DRTY, SRVD, DDLX + * Phase 2: DCLS, DDLA, DRAS, DSFD, DTDC, DTEB, DTSC + * Phase 3: DDLS (view entity — SQL-like) + */ + +// ============================================ +// Base types +// ============================================ + +/** Source location for error reporting */ +export interface SourceLocation { + startOffset: number; + endOffset: number; + startLine: number; + startColumn: number; + endLine?: number; + endColumn?: number; +} + +/** Base AST node */ +export interface AstNode { + loc?: SourceLocation; +} + +// ============================================ +// Annotations +// ============================================ + +/** Annotation value variants */ +export type AnnotationValue = + | StringLiteral + | EnumValue + | BooleanLiteral + | NumberLiteral + | AnnotationArray + | AnnotationObject; + +export interface StringLiteral extends AstNode { + kind: 'string'; + value: string; +} + +export interface EnumValue extends AstNode { + kind: 'enum'; + value: string; +} + +export interface BooleanLiteral extends AstNode { + kind: 'boolean'; + value: boolean; +} + +export interface NumberLiteral extends AstNode { + kind: 'number'; + value: number; +} + +export interface AnnotationArray extends AstNode { + kind: 'array'; + items: AnnotationValue[]; +} + +export interface AnnotationObject extends AstNode { + kind: 'object'; + properties: AnnotationProperty[]; +} + +export interface AnnotationProperty extends AstNode { + key: string; + value: AnnotationValue; +} + +/** A single annotation: @DottedKey.path: value */ +export interface Annotation extends AstNode { + key: string; + value: AnnotationValue; +} + +// ============================================ +// Type references +// ============================================ + +/** ABAP built-in type: abap.char(10), abap.dec(11,2) */ +export interface BuiltinTypeRef extends AstNode { + kind: 'builtin'; + name: string; + length?: number; + decimals?: number; +} + +/** Data element / named type reference */ +export interface NamedTypeRef extends AstNode { + kind: 'named'; + name: string; +} + +export type TypeRef = BuiltinTypeRef | NamedTypeRef; + +// ============================================ +// Field definitions +// ============================================ + +/** Field in a table/structure/aspect body */ +export interface FieldDefinition extends AstNode { + annotations: Annotation[]; + name: string; + type: TypeRef; + isKey: boolean; + notNull: boolean; +} + +/** Include directive: include ; */ +export interface IncludeDirective extends AstNode { + kind: 'include'; + name: string; +} + +export type TableMember = FieldDefinition | IncludeDirective; + +// ============================================ +// Top-level definitions (Phase 1) +// ============================================ + +/** define table { ... } */ +export interface TableDefinition extends AstNode { + kind: 'table'; + name: string; + annotations: Annotation[]; + members: TableMember[]; +} + +/** define structure { ... } */ +export interface StructureDefinition extends AstNode { + kind: 'structure'; + name: string; + annotations: Annotation[]; + members: TableMember[]; +} + +/** define type : ; */ +export interface SimpleTypeDefinition extends AstNode { + kind: 'simpleType'; + name: string; + annotations: Annotation[]; + type: TypeRef; +} + +/** define service { expose ...; } */ +export interface ServiceDefinition extends AstNode { + kind: 'service'; + name: string; + annotations: Annotation[]; + exposes: ExposeStatement[]; +} + +/** expose as ; */ +export interface ExposeStatement extends AstNode { + entity: string; + alias?: string; +} + +/** annotate entity with { ... } */ +export interface MetadataExtension extends AstNode { + kind: 'metadataExtension'; + entity: string; + annotations: Annotation[]; + elements: AnnotatedElement[]; +} + +/** Element inside a metadata extension: @Anno field; */ +export interface AnnotatedElement extends AstNode { + annotations: Annotation[]; + name: string; +} + +// ============================================ +// Future Phase 2+ types (placeholders) +// ============================================ + +/** define role { grant ... } */ +export interface RoleDefinition extends AstNode { + kind: 'role'; + name: string; + // Phase 2: grant clauses, conditions +} + +/** define view entity as select from ... { ... } */ +export interface ViewEntityDefinition extends AstNode { + kind: 'viewEntity'; + name: string; + annotations: Annotation[]; + // Phase 3: datasource, fields, joins, associations +} + +// ============================================ +// Union type for all definitions +// ============================================ + +export type CdsDefinition = + | TableDefinition + | StructureDefinition + | SimpleTypeDefinition + | ServiceDefinition + | MetadataExtension + | RoleDefinition + | ViewEntityDefinition; + +// ============================================ +// Source file (root AST node) +// ============================================ + +/** Root AST node representing a parsed CDS source file */ +export interface CdsSourceFile extends AstNode { + definitions: CdsDefinition[]; +} diff --git a/packages/acds/src/errors.ts b/packages/acds/src/errors.ts new file mode 100644 index 00000000..d69cc6c1 --- /dev/null +++ b/packages/acds/src/errors.ts @@ -0,0 +1,33 @@ +/** + * Parse error types for ABAP CDS + */ +import type { ILexingError, IRecognitionException } from 'chevrotain'; + +export interface CdsParseError { + message: string; + line: number; + column: number; + offset: number; + length: number; +} + +export function fromLexError(err: ILexingError): CdsParseError { + return { + message: err.message, + line: err.line ?? 1, + column: err.column ?? 1, + offset: err.offset, + length: err.length, + }; +} + +export function fromParseError(err: IRecognitionException): CdsParseError { + const token = err.token; + return { + message: err.message, + line: token.startLine ?? 1, + column: token.startColumn ?? 1, + offset: token.startOffset, + length: (token.endOffset ?? token.startOffset) - token.startOffset + 1, + }; +} diff --git a/packages/acds/src/index.ts b/packages/acds/src/index.ts new file mode 100644 index 00000000..29c02292 --- /dev/null +++ b/packages/acds/src/index.ts @@ -0,0 +1,101 @@ +/** + * @abapify/acds — ABAP CDS Source Parser + * + * Parses ABAP CDS DDL source (.acds) files into a typed AST. + * + * @example + * ```typescript + * import { parse } from '@abapify/acds'; + * + * const result = parse(` + * @AbapCatalog.tableCategory : #TRANSPARENT + * define table ztable { + * key field1 : abap.char(10) not null; + * field2 : some_data_element; + * } + * `); + * + * if (result.errors.length === 0) { + * const table = result.ast.definitions[0]; // TableDefinition + * } + * ``` + */ + +import { CdsLexer } from './tokens'; +import { cdsParser } from './parser'; +import { cdsVisitor } from './visitor'; +import type { CdsSourceFile } from './ast'; +import type { CdsParseError } from './errors'; +import { fromLexError, fromParseError } from './errors'; + +/** Result of parsing a CDS source file */ +export interface ParseResult { + /** The parsed AST (may be partial if there are errors) */ + ast: CdsSourceFile; + /** Lexing and parsing errors */ + errors: CdsParseError[]; +} + +/** + * Parse an ABAP CDS source string into a typed AST. + * + * @param source - The CDS source code to parse + * @returns Parse result with AST and any errors + */ +export function parse(source: string): ParseResult { + // Step 1: Tokenize + const lexResult = CdsLexer.tokenize(source); + const errors: CdsParseError[] = lexResult.errors.map(fromLexError); + + // Step 2: Parse tokens → CST + cdsParser.input = lexResult.tokens; + const cst = cdsParser.sourceFile(); + errors.push(...cdsParser.errors.map(fromParseError)); + + // Step 3: CST → AST + let ast: CdsSourceFile = { definitions: [] }; + if (cst) { + try { + ast = cdsVisitor.visit(cst) as CdsSourceFile; + } catch { + // Visitor may fail on severely malformed CST + // Errors already captured from parser + } + } + + return { ast, errors }; +} + +// Re-export types +export type { + CdsSourceFile, + CdsDefinition, + TableDefinition, + StructureDefinition, + SimpleTypeDefinition, + ServiceDefinition, + MetadataExtension, + ViewEntityDefinition, + RoleDefinition, + FieldDefinition, + IncludeDirective, + TableMember, + ExposeStatement, + AnnotatedElement, + Annotation, + AnnotationValue, + StringLiteral, + EnumValue, + BooleanLiteral, + NumberLiteral, + AnnotationArray, + AnnotationObject, + AnnotationProperty, + TypeRef, + BuiltinTypeRef, + NamedTypeRef, + SourceLocation, + AstNode, +} from './ast'; + +export type { CdsParseError } from './errors'; diff --git a/packages/acds/src/parser.test.ts b/packages/acds/src/parser.test.ts new file mode 100644 index 00000000..6277b4dc --- /dev/null +++ b/packages/acds/src/parser.test.ts @@ -0,0 +1,481 @@ +import { describe, it, expect } from 'vitest'; +import { parse } from './index'; +import type { + TableDefinition, + StructureDefinition, + SimpleTypeDefinition, + ServiceDefinition, + MetadataExtension, + FieldDefinition, +} from './ast'; + +// ============================================ +// Table definitions +// ============================================ + +describe('table definition', () => { + it('parses a basic table with key fields', () => { + const result = parse(` + define table ztable { + key field1 : abap.char(10) not null; + field2 : some_data_element; + } + `); + + expect(result.errors).toHaveLength(0); + expect(result.ast.definitions).toHaveLength(1); + + const table = result.ast.definitions[0] as TableDefinition; + expect(table.kind).toBe('table'); + expect(table.name).toBe('ztable'); + expect(table.members).toHaveLength(2); + + const f1 = table.members[0] as FieldDefinition; + expect(f1.name).toBe('field1'); + expect(f1.isKey).toBe(true); + expect(f1.notNull).toBe(true); + expect(f1.type).toEqual({ + kind: 'builtin', + name: 'char', + length: 10, + decimals: undefined, + }); + + const f2 = table.members[1] as FieldDefinition; + expect(f2.name).toBe('field2'); + expect(f2.isKey).toBe(false); + expect(f2.notNull).toBe(false); + expect(f2.type).toEqual({ kind: 'named', name: 'some_data_element' }); + }); + + it('parses table with annotations', () => { + const result = parse(` + @EndUserText.label : 'Test table' + @AbapCatalog.tableCategory : #TRANSPARENT + @AbapCatalog.deliveryClass : #A + @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE + define table ztable { + key mandt : abap.clnt not null; + key field1 : abap.char(10) not null; + } + `); + + expect(result.errors).toHaveLength(0); + + const table = result.ast.definitions[0] as TableDefinition; + expect(table.kind).toBe('table'); + expect(table.name).toBe('ztable'); + expect(table.annotations).toHaveLength(4); + + expect(table.annotations[0].key).toBe('EndUserText.label'); + expect(table.annotations[0].value).toEqual({ + kind: 'string', + value: 'Test table', + }); + + expect(table.annotations[1].key).toBe('AbapCatalog.tableCategory'); + expect(table.annotations[1].value).toEqual({ + kind: 'enum', + value: 'TRANSPARENT', + }); + + expect(table.annotations[2].key).toBe('AbapCatalog.deliveryClass'); + expect(table.annotations[2].value).toEqual({ kind: 'enum', value: 'A' }); + + expect(table.annotations[3].key).toBe('AbapCatalog.enhancement.category'); + expect(table.annotations[3].value).toEqual({ + kind: 'enum', + value: 'NOT_EXTENSIBLE', + }); + }); + + it('parses table with decimal type', () => { + const result = parse(` + define table ztable { + amount : abap.dec(11,2); + } + `); + + expect(result.errors).toHaveLength(0); + + const table = result.ast.definitions[0] as TableDefinition; + const field = table.members[0] as FieldDefinition; + expect(field.type).toEqual({ + kind: 'builtin', + name: 'dec', + length: 11, + decimals: 2, + }); + }); + + it('parses table with include directive', () => { + const result = parse(` + define table ztable { + include some_structure; + key field1 : abap.char(10) not null; + } + `); + + expect(result.errors).toHaveLength(0); + + const table = result.ast.definitions[0] as TableDefinition; + expect(table.members).toHaveLength(2); + expect(table.members[0]).toEqual({ + kind: 'include', + name: 'some_structure', + }); + }); + + it('parses table with builtin types without length', () => { + const result = parse(` + define table ztable { + key mandt : abap.clnt not null; + date_field : abap.dats; + time_field : abap.tims; + int_field : abap.int4; + str_field : abap.string; + } + `); + + expect(result.errors).toHaveLength(0); + + const table = result.ast.definitions[0] as TableDefinition; + expect(table.members).toHaveLength(5); + + const clnt = (table.members[0] as FieldDefinition).type; + expect(clnt).toEqual({ + kind: 'builtin', + name: 'clnt', + length: undefined, + decimals: undefined, + }); + + const dats = (table.members[1] as FieldDefinition).type; + expect(dats).toEqual({ + kind: 'builtin', + name: 'dats', + length: undefined, + decimals: undefined, + }); + }); +}); + +// ============================================ +// Structure definitions +// ============================================ + +describe('structure definition', () => { + it('parses a basic structure', () => { + const result = parse(` + define structure zstruct { + field1 : abap.char(20); + field2 : abap.numc(8); + } + `); + + expect(result.errors).toHaveLength(0); + + const struct = result.ast.definitions[0] as StructureDefinition; + expect(struct.kind).toBe('structure'); + expect(struct.name).toBe('zstruct'); + expect(struct.members).toHaveLength(2); + + const f1 = struct.members[0] as FieldDefinition; + expect(f1.name).toBe('field1'); + expect(f1.isKey).toBe(false); // structures don't have keys typically + expect(f1.type).toEqual({ + kind: 'builtin', + name: 'char', + length: 20, + decimals: undefined, + }); + }); + + it('parses structure with annotations', () => { + const result = parse(` + @EndUserText.label : 'A structure' + define structure zstruct { + field1 : some_element; + } + `); + + expect(result.errors).toHaveLength(0); + + const struct = result.ast.definitions[0] as StructureDefinition; + expect(struct.annotations).toHaveLength(1); + expect(struct.annotations[0].key).toBe('EndUserText.label'); + }); +}); + +// ============================================ +// Simple type definitions (DRTY) +// ============================================ + +describe('simple type definition', () => { + it('parses define type with builtin type', () => { + const result = parse(` + @EndUserText.label : 'This is a test label simple type' + @EndUserText.quickInfo : 'This is the quick info for the simple type' + define type z_aff_example_drty : abap.char(10); + `); + + expect(result.errors).toHaveLength(0); + + const typeDef = result.ast.definitions[0] as SimpleTypeDefinition; + expect(typeDef.kind).toBe('simpleType'); + expect(typeDef.name).toBe('z_aff_example_drty'); + expect(typeDef.annotations).toHaveLength(2); + expect(typeDef.type).toEqual({ + kind: 'builtin', + name: 'char', + length: 10, + decimals: undefined, + }); + }); + + it('parses define type with named type', () => { + const result = parse(` + define type zmytype : some_data_element; + `); + + expect(result.errors).toHaveLength(0); + + const typeDef = result.ast.definitions[0] as SimpleTypeDefinition; + expect(typeDef.kind).toBe('simpleType'); + expect(typeDef.name).toBe('zmytype'); + expect(typeDef.type).toEqual({ kind: 'named', name: 'some_data_element' }); + }); +}); + +// ============================================ +// Service definitions (SRVD) +// ============================================ + +describe('service definition', () => { + it('parses service with expose statements', () => { + const result = parse(` + @EndUserText.label: 'Example' + define service z_aff_example_srvd { + expose z_aff_example_ddls as myEntity; + } + `); + + expect(result.errors).toHaveLength(0); + + const svc = result.ast.definitions[0] as ServiceDefinition; + expect(svc.kind).toBe('service'); + expect(svc.name).toBe('z_aff_example_srvd'); + expect(svc.annotations).toHaveLength(1); + expect(svc.exposes).toHaveLength(1); + expect(svc.exposes[0].entity).toBe('z_aff_example_ddls'); + expect(svc.exposes[0].alias).toBe('myEntity'); + }); + + it('parses service with multiple exposes', () => { + const result = parse(` + define service zsvc { + expose entity1 as Alias1; + expose entity2 as Alias2; + expose entity3 as Alias3; + } + `); + + expect(result.errors).toHaveLength(0); + + const svc = result.ast.definitions[0] as ServiceDefinition; + expect(svc.exposes).toHaveLength(3); + expect(svc.exposes[0]).toEqual({ entity: 'entity1', alias: 'Alias1' }); + expect(svc.exposes[1]).toEqual({ entity: 'entity2', alias: 'Alias2' }); + expect(svc.exposes[2]).toEqual({ entity: 'entity3', alias: 'Alias3' }); + }); + + it('parses expose without alias', () => { + const result = parse(` + define service zsvc { + expose myentity; + } + `); + + expect(result.errors).toHaveLength(0); + + const svc = result.ast.definitions[0] as ServiceDefinition; + expect(svc.exposes[0].entity).toBe('myentity'); + expect(svc.exposes[0].alias).toBeUndefined(); + }); +}); + +// ============================================ +// Metadata extension (DDLX) +// ============================================ + +describe('metadata extension', () => { + it('parses annotate entity with annotated elements', () => { + const result = parse(` + @Metadata.layer: #CORE + annotate entity Z_AFF_EXAMPLE_DDLX + with + { + @EndUserText.label: 'Carrier ID' + Carrid; + } + `); + + expect(result.errors).toHaveLength(0); + + const ext = result.ast.definitions[0] as MetadataExtension; + expect(ext.kind).toBe('metadataExtension'); + expect(ext.entity).toBe('Z_AFF_EXAMPLE_DDLX'); + expect(ext.annotations).toHaveLength(1); + expect(ext.annotations[0].key).toBe('Metadata.layer'); + expect(ext.annotations[0].value).toEqual({ kind: 'enum', value: 'CORE' }); + + expect(ext.elements).toHaveLength(1); + expect(ext.elements[0].name).toBe('Carrid'); + expect(ext.elements[0].annotations).toHaveLength(1); + expect(ext.elements[0].annotations[0].key).toBe('EndUserText.label'); + }); + + it('parses multiple annotated elements', () => { + const result = parse(` + @Metadata.layer: #CORE + annotate entity ZMyView with + { + @EndUserText.label: 'Field A' + FieldA; + @EndUserText.label: 'Field B' + @UI.hidden: true + FieldB; + } + `); + + expect(result.errors).toHaveLength(0); + + const ext = result.ast.definitions[0] as MetadataExtension; + expect(ext.elements).toHaveLength(2); + expect(ext.elements[0].name).toBe('FieldA'); + expect(ext.elements[0].annotations).toHaveLength(1); + expect(ext.elements[1].name).toBe('FieldB'); + expect(ext.elements[1].annotations).toHaveLength(2); + expect(ext.elements[1].annotations[1].key).toBe('UI.hidden'); + expect(ext.elements[1].annotations[1].value).toEqual({ + kind: 'boolean', + value: true, + }); + }); +}); + +// ============================================ +// Annotation values +// ============================================ + +describe('annotation values', () => { + it('parses string literal annotation', () => { + const result = parse(` + @EndUserText.label : 'Hello World' + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + const def = result.ast.definitions[0] as SimpleTypeDefinition; + expect(def.annotations[0].value).toEqual({ + kind: 'string', + value: 'Hello World', + }); + }); + + it('parses enum annotation', () => { + const result = parse(` + @AbapCatalog.tableCategory : #TRANSPARENT + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + const def = result.ast.definitions[0] as SimpleTypeDefinition; + expect(def.annotations[0].value).toEqual({ + kind: 'enum', + value: 'TRANSPARENT', + }); + }); + + it('parses boolean annotation', () => { + const result = parse(` + @AbapCatalog.entityBuffer.definitionAllowed: true + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + const def = result.ast.definitions[0] as SimpleTypeDefinition; + expect(def.annotations[0].value).toEqual({ kind: 'boolean', value: true }); + }); + + it('parses number annotation', () => { + const result = parse(` + @SomeAnnotation.count: 42 + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + const def = result.ast.definitions[0] as SimpleTypeDefinition; + expect(def.annotations[0].value).toEqual({ kind: 'number', value: 42 }); + }); + + it('parses array annotation', () => { + const result = parse(` + @Scope: [#VIEW, #ENTITY] + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + const def = result.ast.definitions[0] as SimpleTypeDefinition; + expect(def.annotations[0].value).toEqual({ + kind: 'array', + items: [ + { kind: 'enum', value: 'VIEW' }, + { kind: 'enum', value: 'ENTITY' }, + ], + }); + }); +}); + +// ============================================ +// Comments +// ============================================ + +describe('comments', () => { + it('ignores line comments', () => { + const result = parse(` + // This is a comment + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + expect(result.ast.definitions).toHaveLength(1); + }); + + it('ignores block comments', () => { + const result = parse(` + /* Multi-line + comment */ + define type ztest : abap.char(1); + `); + + expect(result.errors).toHaveLength(0); + expect(result.ast.definitions).toHaveLength(1); + }); +}); + +// ============================================ +// Error handling +// ============================================ + +describe('error handling', () => { + it('reports errors for invalid input', () => { + const result = parse('this is not valid CDS'); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('reports errors for incomplete input', () => { + const result = parse('define table ztable {'); + expect(result.errors.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/acds/src/parser.ts b/packages/acds/src/parser.ts new file mode 100644 index 00000000..7a9b6d8b --- /dev/null +++ b/packages/acds/src/parser.ts @@ -0,0 +1,333 @@ +/** + * Chevrotain CstParser for ABAP CDS + * + * Phase 1: table, structure, simpleType, service, metadataExtension + */ +import { CstParser } from 'chevrotain'; +import { + allTokens, + At, + Colon, + Semicolon, + Dot, + Comma, + LBrace, + RBrace, + LParen, + RParen, + LBracket, + RBracket, + Define, + Table, + Structure, + Type, + Service, + Expose, + Annotate, + Entity, + With, + Key, + Not, + Null, + As, + Include, + True, + False, + Abap, + Identifier, + StringLiteral, + NumberLiteral, + EnumLiteral, +} from './tokens'; + +export class CdsParser extends CstParser { + constructor() { + super(allTokens, { + recoveryEnabled: true, + maxLookahead: 3, + }); + this.performSelfAnalysis(); + } + + // ============================================ + // Root rule + // ============================================ + + public sourceFile = this.RULE('sourceFile', () => { + this.MANY(() => { + this.SUBRULE(this.topLevelAnnotation); + }); + this.SUBRULE(this.definition); + }); + + // ============================================ + // Top-level definition dispatch + // ============================================ + + private definition = this.RULE('definition', () => { + this.OR([ + { ALT: () => this.SUBRULE(this.defineStatement) }, + { ALT: () => this.SUBRULE(this.annotateStatement) }, + ]); + }); + + private defineStatement = this.RULE('defineStatement', () => { + this.CONSUME(Define); + this.OR([ + { ALT: () => this.SUBRULE(this.tableDefinition) }, + { ALT: () => this.SUBRULE(this.structureDefinition) }, + { ALT: () => this.SUBRULE(this.simpleTypeDefinition) }, + { ALT: () => this.SUBRULE(this.serviceDefinition) }, + ]); + }); + + // ============================================ + // Table definition + // ============================================ + + private tableDefinition = this.RULE('tableDefinition', () => { + this.CONSUME(Table); + this.SUBRULE(this.cdsName); + this.CONSUME(LBrace); + this.MANY(() => { + this.SUBRULE(this.tableMember); + }); + this.CONSUME(RBrace); + }); + + // ============================================ + // Structure definition + // ============================================ + + private structureDefinition = this.RULE('structureDefinition', () => { + this.CONSUME(Structure); + this.SUBRULE(this.cdsName); + this.CONSUME(LBrace); + this.MANY(() => { + this.SUBRULE(this.tableMember); + }); + this.CONSUME(RBrace); + }); + + // ============================================ + // Table/structure members + // ============================================ + + private tableMember = this.RULE('tableMember', () => { + this.OR([ + { ALT: () => this.SUBRULE(this.includeDirective) }, + { ALT: () => this.SUBRULE(this.fieldDefinition) }, + ]); + }); + + private includeDirective = this.RULE('includeDirective', () => { + this.CONSUME(Include); + this.SUBRULE(this.cdsName); + this.CONSUME(Semicolon); + }); + + private fieldDefinition = this.RULE('fieldDefinition', () => { + this.MANY(() => { + this.SUBRULE(this.annotation); + }); + this.OPTION(() => { + this.CONSUME(Key); + }); + this.SUBRULE(this.cdsName); + this.CONSUME(Colon); + this.SUBRULE(this.typeReference); + this.OPTION2(() => { + this.CONSUME(Not); + this.CONSUME(Null); + }); + this.CONSUME(Semicolon); + }); + + // ============================================ + // Simple type definition + // ============================================ + + private simpleTypeDefinition = this.RULE('simpleTypeDefinition', () => { + this.CONSUME(Type); + this.SUBRULE(this.cdsName); + this.CONSUME(Colon); + this.SUBRULE(this.typeReference); + this.CONSUME(Semicolon); + }); + + // ============================================ + // Service definition + // ============================================ + + private serviceDefinition = this.RULE('serviceDefinition', () => { + this.CONSUME(Service); + this.SUBRULE(this.cdsName); + this.CONSUME(LBrace); + this.MANY(() => { + this.SUBRULE(this.exposeStatement); + }); + this.CONSUME(RBrace); + }); + + private exposeStatement = this.RULE('exposeStatement', () => { + this.CONSUME(Expose); + this.SUBRULE(this.cdsName); + this.OPTION(() => { + this.CONSUME(As); + this.SUBRULE2(this.cdsName); + }); + this.CONSUME(Semicolon); + }); + + // ============================================ + // Metadata extension (annotate entity ... with { ... }) + // ============================================ + + private annotateStatement = this.RULE('annotateStatement', () => { + this.CONSUME(Annotate); + this.CONSUME(Entity); + this.SUBRULE(this.cdsName); + this.CONSUME(With); + this.CONSUME(LBrace); + this.MANY(() => { + this.SUBRULE(this.annotatedElement); + }); + this.CONSUME(RBrace); + }); + + private annotatedElement = this.RULE('annotatedElement', () => { + this.MANY(() => { + this.SUBRULE(this.annotation); + }); + this.SUBRULE(this.cdsName); + this.CONSUME(Semicolon); + }); + + // ============================================ + // Annotations + // ============================================ + + private topLevelAnnotation = this.RULE('topLevelAnnotation', () => { + this.SUBRULE(this.annotation); + }); + + private annotation = this.RULE('annotation', () => { + this.CONSUME(At); + this.SUBRULE(this.dottedName); + this.OPTION(() => { + this.CONSUME(Colon); + this.SUBRULE(this.annotationValue); + }); + }); + + private dottedName = this.RULE('dottedName', () => { + this.SUBRULE(this.cdsName); + this.MANY(() => { + this.CONSUME(Dot); + this.SUBRULE2(this.cdsName); + }); + }); + + private annotationValue = this.RULE('annotationValue', () => { + this.OR([ + { ALT: () => this.CONSUME(StringLiteral) }, + { ALT: () => this.CONSUME(EnumLiteral) }, + { ALT: () => this.CONSUME(True) }, + { ALT: () => this.CONSUME(False) }, + { ALT: () => this.CONSUME(NumberLiteral) }, + { ALT: () => this.SUBRULE(this.annotationArray) }, + { ALT: () => this.SUBRULE(this.annotationObject) }, + ]); + }); + + private annotationArray = this.RULE('annotationArray', () => { + this.CONSUME(LBracket); + this.MANY_SEP({ + SEP: Comma, + DEF: () => { + this.SUBRULE(this.annotationValue); + }, + }); + this.CONSUME(RBracket); + }); + + private annotationObject = this.RULE('annotationObject', () => { + this.CONSUME(LBrace); + this.MANY_SEP({ + SEP: Comma, + DEF: () => { + this.SUBRULE(this.annotationProperty); + }, + }); + this.CONSUME(RBrace); + }); + + private annotationProperty = this.RULE('annotationProperty', () => { + this.SUBRULE(this.dottedName); + this.CONSUME(Colon); + this.SUBRULE(this.annotationValue); + }); + + // ============================================ + // Type references + // ============================================ + + private typeReference = this.RULE('typeReference', () => { + this.OR([ + { ALT: () => this.SUBRULE(this.builtinType) }, + { ALT: () => this.SUBRULE(this.namedType) }, + ]); + }); + + /** abap.char(10) or abap.dec(11,2) */ + private builtinType = this.RULE('builtinType', () => { + this.CONSUME(Abap); + this.CONSUME(Dot); + this.SUBRULE(this.cdsName); + this.OPTION(() => { + this.CONSUME(LParen); + this.CONSUME(NumberLiteral); + this.OPTION2(() => { + this.CONSUME(Comma); + this.CONSUME2(NumberLiteral); + }); + this.CONSUME(RParen); + }); + }); + + /** Data element or other named type reference */ + private namedType = this.RULE('namedType', () => { + this.SUBRULE(this.qualifiedName); + }); + + // ============================================ + // Name helpers + // ============================================ + + /** An identifier that may also be a keyword used as a name */ + private cdsName = this.RULE('cdsName', () => { + this.OR([ + { ALT: () => this.CONSUME(Identifier) }, + // Keywords that can appear as names in certain contexts + { ALT: () => this.CONSUME(Table) }, + { ALT: () => this.CONSUME(Structure) }, + { ALT: () => this.CONSUME(Type) }, + { ALT: () => this.CONSUME(Service) }, + { ALT: () => this.CONSUME(Entity) }, + { ALT: () => this.CONSUME(Key) }, + { ALT: () => this.CONSUME(Expose) }, + ]); + }); + + /** Dot-separated qualified name: foo.bar.baz */ + private qualifiedName = this.RULE('qualifiedName', () => { + this.SUBRULE(this.cdsName); + this.MANY(() => { + this.CONSUME(Dot); + this.SUBRULE2(this.cdsName); + }); + }); +} + +/** Singleton parser instance (Chevrotain parsers are reusable) */ +export const cdsParser = new CdsParser(); diff --git a/packages/acds/src/tokens.ts b/packages/acds/src/tokens.ts new file mode 100644 index 00000000..68f15dd2 --- /dev/null +++ b/packages/acds/src/tokens.ts @@ -0,0 +1,234 @@ +/** + * Chevrotain Token Definitions for ABAP CDS + * + * Token order matters: more specific patterns must come before general ones. + * Keywords use `longer_alt` to avoid matching prefixes of identifiers. + */ +import { createToken, Lexer } from 'chevrotain'; + +// ============================================ +// Whitespace & Comments +// ============================================ + +export const WhiteSpace = createToken({ + name: 'WhiteSpace', + pattern: /\s+/, + group: Lexer.SKIPPED, +}); + +export const LineComment = createToken({ + name: 'LineComment', + pattern: /\/\/[^\n]*/, + group: Lexer.SKIPPED, +}); + +export const BlockComment = createToken({ + name: 'BlockComment', + pattern: /\/\*[\s\S]*?\*\//, + group: Lexer.SKIPPED, +}); + +// ============================================ +// Literals +// ============================================ + +export const StringLiteral = createToken({ + name: 'StringLiteral', + pattern: /'(?:[^'\\]|\\.)*'/, +}); + +export const NumberLiteral = createToken({ + name: 'NumberLiteral', + pattern: /\d+(?:\.\d+)?/, +}); + +/** Hash-prefixed enum value: #TRANSPARENT, #NOT_EXTENSIBLE */ +export const EnumLiteral = createToken({ + name: 'EnumLiteral', + pattern: /#[A-Za-z_][A-Za-z0-9_]*/, +}); + +// ============================================ +// Identifier (must come AFTER all keywords) +// ============================================ + +export const Identifier = createToken({ + name: 'Identifier', + pattern: /[A-Za-z_][A-Za-z0-9_]*/, +}); + +// ============================================ +// Keywords (use longer_alt: Identifier) +// ============================================ + +export const Define = createToken({ + name: 'Define', + pattern: /define/, + longer_alt: Identifier, +}); + +export const Table = createToken({ + name: 'Table', + pattern: /table/, + longer_alt: Identifier, +}); + +export const Structure = createToken({ + name: 'Structure', + pattern: /structure/, + longer_alt: Identifier, +}); + +export const Type = createToken({ + name: 'Type', + pattern: /type/, + longer_alt: Identifier, +}); + +export const Service = createToken({ + name: 'Service', + pattern: /service/, + longer_alt: Identifier, +}); + +export const Expose = createToken({ + name: 'Expose', + pattern: /expose/, + longer_alt: Identifier, +}); + +export const Annotate = createToken({ + name: 'Annotate', + pattern: /annotate/, + longer_alt: Identifier, +}); + +export const Entity = createToken({ + name: 'Entity', + pattern: /entity/, + longer_alt: Identifier, +}); + +export const With = createToken({ + name: 'With', + pattern: /with/, + longer_alt: Identifier, +}); + +export const Key = createToken({ + name: 'Key', + pattern: /key/, + longer_alt: Identifier, +}); + +export const Not = createToken({ + name: 'Not', + pattern: /not/, + longer_alt: Identifier, +}); + +export const Null = createToken({ + name: 'Null', + pattern: /null/, + longer_alt: Identifier, +}); + +export const As = createToken({ + name: 'As', + pattern: /as/, + longer_alt: Identifier, +}); + +export const Include = createToken({ + name: 'Include', + pattern: /include/, + longer_alt: Identifier, +}); + +export const True = createToken({ + name: 'True', + pattern: /true/, + longer_alt: Identifier, +}); + +export const False = createToken({ + name: 'False', + pattern: /false/, + longer_alt: Identifier, +}); + +export const Abap = createToken({ + name: 'Abap', + pattern: /abap/, + longer_alt: Identifier, +}); + +// ============================================ +// Symbols +// ============================================ + +export const At = createToken({ name: 'At', pattern: /@/ }); +export const Colon = createToken({ name: 'Colon', pattern: /:/ }); +export const Semicolon = createToken({ name: 'Semicolon', pattern: /;/ }); +export const Dot = createToken({ name: 'Dot', pattern: /\./ }); +export const Comma = createToken({ name: 'Comma', pattern: /,/ }); +export const LBrace = createToken({ name: 'LBrace', pattern: /\{/ }); +export const RBrace = createToken({ name: 'RBrace', pattern: /\}/ }); +export const LParen = createToken({ name: 'LParen', pattern: /\(/ }); +export const RParen = createToken({ name: 'RParen', pattern: /\)/ }); +export const LBracket = createToken({ name: 'LBracket', pattern: /\[/ }); +export const RBracket = createToken({ name: 'RBracket', pattern: /\]/ }); + +// ============================================ +// Token list (ORDER MATTERS) +// Keywords must come before Identifier +// ============================================ + +export const allTokens = [ + // Whitespace & comments (skipped) + WhiteSpace, + LineComment, + BlockComment, + + // Literals + StringLiteral, + NumberLiteral, + EnumLiteral, + + // Keywords (before Identifier!) + Define, + Table, + Structure, + Type, + Service, + Expose, + Annotate, + Entity, + With, + Key, + Not, + Null, + As, + Include, + True, + False, + Abap, + + // Identifier (catch-all for names) + Identifier, + + // Symbols + At, + Colon, + Semicolon, + Dot, + Comma, + LBrace, + RBrace, + LParen, + RParen, + LBracket, + RBracket, +]; + +export const CdsLexer = new Lexer(allTokens); diff --git a/packages/acds/src/visitor.ts b/packages/acds/src/visitor.ts new file mode 100644 index 00000000..f2baa119 --- /dev/null +++ b/packages/acds/src/visitor.ts @@ -0,0 +1,277 @@ +/** + * CST → AST Visitor + * + * Transforms Chevrotain's Concrete Syntax Tree into our typed AST. + */ +import type { CstNode, IToken } from 'chevrotain'; +import { cdsParser } from './parser'; +import type { + Annotation, + AnnotationValue, + AnnotationProperty, + AnnotatedElement, + BuiltinTypeRef, + CdsDefinition, + CdsSourceFile, + ExposeStatement, + FieldDefinition, + IncludeDirective, + MetadataExtension, + NamedTypeRef, + ServiceDefinition, + SimpleTypeDefinition, + StructureDefinition, + TableDefinition, + TableMember, + TypeRef, +} from './ast'; + +// Generate the base visitor class from the parser's grammar +const BaseCstVisitor = cdsParser.getBaseCstVisitorConstructor< + unknown, + unknown +>(); + +export class CdsVisitor extends BaseCstVisitor { + constructor() { + super(); + this.validateVisitor(); + } + + sourceFile(ctx: Record): CdsSourceFile { + const annotations: Annotation[] = (ctx.topLevelAnnotation ?? []).map( + (node) => this.visit(node) as Annotation, + ); + const def = this.visit(ctx.definition[0]) as CdsDefinition; + + // Attach top-level annotations to the definition + if ('annotations' in def) { + (def as { annotations: Annotation[] }).annotations = [ + ...annotations, + ...(def as { annotations: Annotation[] }).annotations, + ]; + } + + return { definitions: [def] }; + } + + definition(ctx: Record): CdsDefinition { + if (ctx.defineStatement) { + return this.visit(ctx.defineStatement[0]) as CdsDefinition; + } + return this.visit(ctx.annotateStatement[0]) as CdsDefinition; + } + + defineStatement(ctx: Record): CdsDefinition { + if (ctx.tableDefinition) { + return this.visit(ctx.tableDefinition[0]) as TableDefinition; + } + if (ctx.structureDefinition) { + return this.visit(ctx.structureDefinition[0]) as StructureDefinition; + } + if (ctx.simpleTypeDefinition) { + return this.visit(ctx.simpleTypeDefinition[0]) as SimpleTypeDefinition; + } + return this.visit(ctx.serviceDefinition[0]) as ServiceDefinition; + } + + tableDefinition(ctx: Record): TableDefinition { + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const members = ((ctx.tableMember as CstNode[]) ?? []).map( + (node) => this.visit(node) as TableMember, + ); + return { kind: 'table', name, annotations: [], members }; + } + + structureDefinition( + ctx: Record, + ): StructureDefinition { + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const members = ((ctx.tableMember as CstNode[]) ?? []).map( + (node) => this.visit(node) as TableMember, + ); + return { kind: 'structure', name, annotations: [], members }; + } + + tableMember(ctx: Record): TableMember { + if (ctx.includeDirective) { + return this.visit(ctx.includeDirective[0]) as IncludeDirective; + } + return this.visit(ctx.fieldDefinition[0]) as FieldDefinition; + } + + includeDirective(ctx: Record): IncludeDirective { + const name = this.visit(ctx.cdsName[0]) as string; + return { kind: 'include', name }; + } + + fieldDefinition(ctx: Record): FieldDefinition { + const annotations: Annotation[] = ((ctx.annotation as CstNode[]) ?? []).map( + (node) => this.visit(node) as Annotation, + ); + const isKey = !!(ctx.Key as IToken[] | undefined)?.length; + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const type = this.visit((ctx.typeReference as CstNode[])[0]) as TypeRef; + const notNull = !!(ctx.Not as IToken[] | undefined)?.length; + return { annotations, name, type, isKey, notNull }; + } + + simpleTypeDefinition( + ctx: Record, + ): SimpleTypeDefinition { + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const type = this.visit((ctx.typeReference as CstNode[])[0]) as TypeRef; + return { kind: 'simpleType', name, annotations: [], type }; + } + + serviceDefinition( + ctx: Record, + ): ServiceDefinition { + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const exposes: ExposeStatement[] = ( + (ctx.exposeStatement as CstNode[]) ?? [] + ).map((node) => this.visit(node) as ExposeStatement); + return { kind: 'service', name, annotations: [], exposes }; + } + + exposeStatement(ctx: Record): ExposeStatement { + const names = (ctx.cdsName ?? []).map((node) => this.visit(node) as string); + const entity = names[0]; + const alias = names.length > 1 ? names[1] : undefined; + return { entity, alias }; + } + + annotateStatement( + ctx: Record, + ): MetadataExtension { + const entity = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const elements: AnnotatedElement[] = ( + (ctx.annotatedElement as CstNode[]) ?? [] + ).map((node) => this.visit(node) as AnnotatedElement); + return { kind: 'metadataExtension', entity, annotations: [], elements }; + } + + annotatedElement(ctx: Record): AnnotatedElement { + const annotations: Annotation[] = (ctx.annotation ?? []).map( + (node) => this.visit(node) as Annotation, + ); + const name = this.visit(ctx.cdsName[0]) as string; + return { annotations, name }; + } + + topLevelAnnotation(ctx: Record): Annotation { + return this.visit(ctx.annotation[0]) as Annotation; + } + + annotation(ctx: Record): Annotation { + const key = this.visit((ctx.dottedName as CstNode[])[0]) as string; + const value: AnnotationValue = ctx.annotationValue + ? (this.visit((ctx.annotationValue as CstNode[])[0]) as AnnotationValue) + : { kind: 'boolean', value: true }; + return { key, value }; + } + + dottedName(ctx: Record): string { + return (ctx.cdsName ?? []) + .map((node) => this.visit(node) as string) + .join('.'); + } + + annotationValue(ctx: Record): AnnotationValue { + if (ctx.StringLiteral) { + const raw = (ctx.StringLiteral as IToken[])[0].image; + return { kind: 'string', value: raw.slice(1, -1) }; + } + if (ctx.EnumLiteral) { + const raw = (ctx.EnumLiteral as IToken[])[0].image; + return { kind: 'enum', value: raw.slice(1) }; // strip # + } + if (ctx.True) { + return { kind: 'boolean', value: true }; + } + if (ctx.False) { + return { kind: 'boolean', value: false }; + } + if (ctx.NumberLiteral) { + return { + kind: 'number', + value: parseFloat((ctx.NumberLiteral as IToken[])[0].image), + }; + } + if (ctx.annotationArray) { + return this.visit( + (ctx.annotationArray as CstNode[])[0], + ) as AnnotationValue; + } + return this.visit( + (ctx.annotationObject as CstNode[])[0], + ) as AnnotationValue; + } + + annotationArray(ctx: Record): AnnotationValue { + const items = (ctx.annotationValue ?? []).map( + (node) => this.visit(node) as AnnotationValue, + ); + return { kind: 'array', items }; + } + + annotationObject(ctx: Record): AnnotationValue { + const properties = (ctx.annotationProperty ?? []).map( + (node) => this.visit(node) as AnnotationProperty, + ); + return { kind: 'object', properties }; + } + + annotationProperty(ctx: Record): AnnotationProperty { + const key = this.visit(ctx.dottedName[0]) as string; + const value = this.visit(ctx.annotationValue[0]) as AnnotationValue; + return { key, value }; + } + + typeReference(ctx: Record): TypeRef { + if (ctx.builtinType) { + return this.visit(ctx.builtinType[0]) as BuiltinTypeRef; + } + return this.visit(ctx.namedType[0]) as NamedTypeRef; + } + + builtinType(ctx: Record): BuiltinTypeRef { + const name = this.visit((ctx.cdsName as CstNode[])[0]) as string; + const numberTokens = ctx.NumberLiteral as IToken[] | undefined; + const length = numberTokens?.[0] + ? parseInt(numberTokens[0].image, 10) + : undefined; + const decimals = numberTokens?.[1] + ? parseInt(numberTokens[1].image, 10) + : undefined; + return { kind: 'builtin', name, length, decimals }; + } + + namedType(ctx: Record): NamedTypeRef { + const name = this.visit(ctx.qualifiedName[0]) as string; + return { kind: 'named', name }; + } + + cdsName(ctx: Record): string { + // Return the image of whichever token matched + const token = + ctx.Identifier?.[0] ?? + ctx.Table?.[0] ?? + ctx.Structure?.[0] ?? + ctx.Type?.[0] ?? + ctx.Service?.[0] ?? + ctx.Entity?.[0] ?? + ctx.Key?.[0] ?? + ctx.Expose?.[0]; + return token?.image ?? ''; + } + + qualifiedName(ctx: Record): string { + return (ctx.cdsName ?? []) + .map((node) => this.visit(node) as string) + .join('.'); + } +} + +/** Singleton visitor instance */ +export const cdsVisitor = new CdsVisitor(); diff --git a/packages/acds/tsconfig.json b/packages/acds/tsconfig.json new file mode 100644 index 00000000..c2104f6b --- /dev/null +++ b/packages/acds/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"] +} diff --git a/packages/acds/tsdown.config.ts b/packages/acds/tsdown.config.ts new file mode 100644 index 00000000..ab43cf84 --- /dev/null +++ b/packages/acds/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsdown'; +import baseConfig from '../../tsdown.config.ts'; + +export default defineConfig({ + ...baseConfig, + entry: ['src/index.ts'], +}); diff --git a/packages/acds/vitest.config.ts b/packages/acds/vitest.config.ts new file mode 100644 index 00000000..8e730d50 --- /dev/null +++ b/packages/acds/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); diff --git a/packages/adk/src/base/fetch-utils.ts b/packages/adk/src/base/fetch-utils.ts new file mode 100644 index 00000000..4507a3ed --- /dev/null +++ b/packages/adk/src/base/fetch-utils.ts @@ -0,0 +1,26 @@ +/** + * Utility for handling v2 client fetch responses. + * + * The v2 ADT client's `fetch()` returns parsed data directly (a string for + * text/plain content), not a standard `Response` object. This helper + * normalises both cases so ADK objects work with both client versions. + */ + +/** + * Convert a fetch result to a text string. + * + * - If the result is already a string (v2 client), return it as-is. + * - If the result is a Response-like object (v1 / standard fetch), call `.text()`. + */ +export async function toText(result: unknown): Promise { + if (typeof result === 'string') return result; + if ( + result && + typeof result === 'object' && + 'text' in result && + typeof (result as any).text === 'function' + ) { + return (result as Response).text(); + } + return String(result ?? ''); +} diff --git a/packages/adk/src/base/model.ts b/packages/adk/src/base/model.ts index 18fcf40c..32068203 100644 --- a/packages/adk/src/base/model.ts +++ b/packages/adk/src/base/model.ts @@ -15,6 +15,7 @@ import type { AdkContext } from './context'; import type { AdkKind } from './kinds'; +import { toText } from './fetch-utils'; /** * Lock handle returned by lock operations @@ -680,7 +681,7 @@ export abstract class AdkObject { `${this.objectUri}/source/main`, { method: 'GET', headers: { Accept: 'text/plain' } }, ); - const currentSource = await response.text(); + const currentSource = await toText(response); if ( this.normalizeSource(currentSource) === this.normalizeSource(self._pendingSource) @@ -793,14 +794,17 @@ export abstract class AdkObject { `; - await this.ctx.client.fetch('/sap/bc/adt/activation', { - method: 'POST', - headers: { - 'Content-Type': 'application/xml', - Accept: 'application/xml', + await this.ctx.client.fetch( + '/sap/bc/adt/activation?method=activate&preauditRequested=true', + { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + Accept: 'application/xml', + }, + body: activationXml, }, - body: activationXml, - }); + ); return this; } diff --git a/packages/adk/src/base/object-set.ts b/packages/adk/src/base/object-set.ts index 4d8ad8bc..24675a2b 100644 --- a/packages/adk/src/base/object-set.ts +++ b/packages/adk/src/base/object-set.ts @@ -250,14 +250,17 @@ export class AdkObjectSet { `; try { - const response = await this.ctx.client.fetch('/sap/bc/adt/activation', { - method: 'POST', - headers: { - 'Content-Type': 'application/xml', - Accept: 'application/xml', + const response = await this.ctx.client.fetch( + '/sap/bc/adt/activation?method=activate&preauditRequested=true', + { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + Accept: 'application/xml', + }, + body: activationXml, }, - body: activationXml, - }); + ); // NOTE: Could parse actual response XML for detailed messages return { diff --git a/packages/adk/src/base/registry.ts b/packages/adk/src/base/registry.ts index 838c4e63..c779b31d 100644 --- a/packages/adk/src/base/registry.ts +++ b/packages/adk/src/base/registry.ts @@ -25,10 +25,17 @@ export type AdkObjectConstructor< T extends AdkObject = AdkObject, > = new (ctx: AdkContext, nameOrData: string | any) => T; +/** How to transform object name for the URI path segment */ +export type NameTransform = 'lowercase' | 'preserve'; + /** Registry entry with constructor and kind */ export interface RegistryEntry { readonly kind: AdkKind; readonly constructor: AdkObjectConstructor; + /** ADT REST endpoint path segment (e.g., 'oo/classes', 'ddic/tabletypes') */ + readonly endpoint?: string; + /** How to transform the object name in URIs (default: lowercase) */ + readonly nameTransform?: NameTransform; } // ============================================ @@ -79,21 +86,36 @@ const adtToKind = new Map(); /** ADK kind to ADT main type mapping (reverse) */ const kindToAdt = new Map(); +/** Options for registerObjectType */ +export interface RegisterObjectTypeOptions { + /** ADT REST endpoint path segment (e.g., 'oo/classes', 'ddic/tabletypes') */ + endpoint?: string; + /** How to transform the object name in URIs (default: 'lowercase') */ + nameTransform?: NameTransform; +} + /** * Register an ADK object type * - * @param adtMainType - Main ADT type (e.g., "DEVC", "CLAS") + * @param adtType - ADT type, either main (e.g., "CLAS") or full (e.g., "TABL/DS") * @param kind - ADK kind constant * @param constructor - Object constructor + * @param options - Optional endpoint and name transform settings */ export function registerObjectType( - adtMainType: string, + adtType: string, kind: AdkKind, constructor: AdkObjectConstructor, + options?: RegisterObjectTypeOptions, ): void { - const normalizedType = adtMainType.toUpperCase(); + const normalizedType = adtType.toUpperCase(); - registry.set(normalizedType, { kind, constructor }); + registry.set(normalizedType, { + kind, + constructor, + endpoint: options?.endpoint, + nameTransform: options?.nameTransform, + }); adtToKind.set(normalizedType, kind); kindToAdt.set(kind, normalizedType); } @@ -101,10 +123,18 @@ export function registerObjectType( /** * Resolve ADT type to registry entry * + * Tries full type first (e.g., "TABL/DS"), then falls back to main type (e.g., "TABL"). + * This allows registering subtypes with different constructors/endpoints. + * * @param adtType - Full or main ADT type (e.g., "DEVC/K" or "DEVC") * @returns Registry entry or undefined if not found */ export function resolveType(adtType: string): RegistryEntry | undefined { + const normalized = adtType.toUpperCase(); + // Try full type first (e.g., "TABL/DS") + const fullMatch = registry.get(normalized); + if (fullMatch) return fullMatch; + // Fall back to main type (e.g., "TABL") const mainType = getMainType(adtType); return registry.get(mainType); } @@ -156,6 +186,36 @@ export function getRegisteredKinds(): AdkKind[] { return Array.from(kindToAdt.keys()); } +/** + * Get ADT REST endpoint for a type (e.g., 'oo/classes', 'ddic/tabletypes') + */ +export function getEndpointForType(adtType: string): string | undefined { + return registry.get(getMainType(adtType))?.endpoint; +} + +/** + * Build the full ADT object URI for a given type and name. + * + * @example + * getObjectUri('CLAS', 'ZCL_MY_CLASS') // '/sap/bc/adt/oo/classes/zcl_my_class' + * getObjectUri('DEVC', 'ZPACKAGE') // '/sap/bc/adt/packages/ZPACKAGE' + * getObjectUri('TTYP', 'ZAGE_STRING_TABLE') // '/sap/bc/adt/ddic/tabletypes/zage_string_table' + */ +export function getObjectUri( + adtType: string, + name: string, +): string | undefined { + const entry = resolveType(adtType); + if (!entry?.endpoint) return undefined; + + const transformedName = + entry.nameTransform === 'preserve' + ? encodeURIComponent(name) + : encodeURIComponent(name.toLowerCase()); + + return `/sap/bc/adt/${entry.endpoint}/${transformedName}`; +} + // ============================================ // Built-in Registrations // ============================================ diff --git a/packages/adk/src/index.ts b/packages/adk/src/index.ts index 5d63927c..0e78a25d 100644 --- a/packages/adk/src/index.ts +++ b/packages/adk/src/index.ts @@ -168,7 +168,11 @@ export { isTypeRegistered, getRegisteredTypes, getRegisteredKinds, + getEndpointForType, + getObjectUri, ADT_TYPE_MAPPINGS, + type NameTransform, + type RegisterObjectTypeOptions, } from './base/registry'; // ADK kinds and type mapping diff --git a/packages/adk/src/objects/ddic/doma/doma.model.ts b/packages/adk/src/objects/ddic/doma/doma.model.ts index c9d1507e..cd7a7baf 100644 --- a/packages/adk/src/objects/ddic/doma/doma.model.ts +++ b/packages/adk/src/objects/ddic/doma/doma.model.ts @@ -52,4 +52,4 @@ export class AdkDomain extends AdkMainObject { // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('DOMA', DomainKind, AdkDomain); +registerObjectType('DOMA', DomainKind, AdkDomain, { endpoint: 'ddic/domains' }); diff --git a/packages/adk/src/objects/ddic/dtel/dtel.model.ts b/packages/adk/src/objects/ddic/dtel/dtel.model.ts index b108d756..8f31bfe7 100644 --- a/packages/adk/src/objects/ddic/dtel/dtel.model.ts +++ b/packages/adk/src/objects/ddic/dtel/dtel.model.ts @@ -56,4 +56,6 @@ export class AdkDataElement extends AdkMainObject< // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('DTEL', DataElementKind, AdkDataElement); +registerObjectType('DTEL', DataElementKind, AdkDataElement, { + endpoint: 'ddic/dataelements', +}); diff --git a/packages/adk/src/objects/ddic/tabl/tabl.model.ts b/packages/adk/src/objects/ddic/tabl/tabl.model.ts index b9599fbd..97f316f1 100644 --- a/packages/adk/src/objects/ddic/tabl/tabl.model.ts +++ b/packages/adk/src/objects/ddic/tabl/tabl.model.ts @@ -23,6 +23,7 @@ import { getGlobalContext } from '../../../base/global-context'; import type { AdkContext } from '../../../base/context'; import type { TableResponse } from '../../../base/adt'; +import { toText } from '../../../base/fetch-utils'; /** * Table/Structure data type - unwrap from blueSource wrapper root element @@ -40,6 +41,39 @@ export class AdkTable extends AdkMainObject { return `/sap/bc/adt/ddic/tables/${encodeURIComponent(this.name.toLowerCase())}`; } + /** + * Get CDS-style table source code from SAP + * Returns the ABAP source definition (annotations + field definitions) + */ + async getSource(): Promise { + return this.lazy('source', async () => { + const response = await this.ctx.client.fetch( + `${this.objectUri}/source/main`, + { method: 'GET', headers: { Accept: 'text/plain' } }, + ); + return toText(response); + }); + } + + /** + * Get table technical settings (DD09L data) + * Returns XML from /sap/bc/adt/ddic/db/settings/{name} + */ + async getSettings(): Promise { + return this.lazy('settings', async () => { + const response = await this.ctx.client.fetch( + `/sap/bc/adt/ddic/db/settings/${encodeURIComponent(this.name.toLowerCase())}`, + { + method: 'GET', + headers: { + Accept: 'application/vnd.sap.adt.table.settings.v2+xml', + }, + }, + ); + return toText(response); + }); + } + protected override get wrapperKey() { return 'blueSource'; } @@ -67,6 +101,20 @@ export class AdkStructure extends AdkMainObject< return `/sap/bc/adt/ddic/structures/${encodeURIComponent(this.name.toLowerCase())}`; } + /** + * Get CDS-style structure source code from SAP + * Returns the ABAP source definition (annotations + field definitions) + */ + async getSource(): Promise { + return this.lazy('source', async () => { + const response = await this.ctx.client.fetch( + `${this.objectUri}/source/main`, + { method: 'GET', headers: { Accept: 'text/plain' } }, + ); + return toText(response); + }); + } + protected override get wrapperKey() { return 'blueSource'; } @@ -82,6 +130,7 @@ export class AdkStructure extends AdkMainObject< // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('TABL', TableKind, AdkTable); -// Note: Structure uses same main type TABL but different ADK kind -// Registration uses TABL main type - structures will be resolved via subtype logic +registerObjectType('TABL', TableKind, AdkTable, { endpoint: 'ddic/tables' }); +registerObjectType('TABL/DS', StructureKind, AdkStructure, { + endpoint: 'ddic/structures', +}); diff --git a/packages/adk/src/objects/ddic/ttyp/ttyp.model.ts b/packages/adk/src/objects/ddic/ttyp/ttyp.model.ts index d94ba1f8..2171ba35 100644 --- a/packages/adk/src/objects/ddic/ttyp/ttyp.model.ts +++ b/packages/adk/src/objects/ddic/ttyp/ttyp.model.ts @@ -55,4 +55,6 @@ export class AdkTableType extends AdkMainObject< // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('TTYP', TableTypeKind, AdkTableType); +registerObjectType('TTYP', TableTypeKind, AdkTableType, { + endpoint: 'ddic/tabletypes', +}); diff --git a/packages/adk/src/objects/repository/clas/clas.model.ts b/packages/adk/src/objects/repository/clas/clas.model.ts index 6e9ebcd1..e600cead 100644 --- a/packages/adk/src/objects/repository/clas/clas.model.ts +++ b/packages/adk/src/objects/repository/clas/clas.model.ts @@ -256,4 +256,4 @@ export class AdkClass extends AdkMainObject { // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('CLAS', ClassKind, AdkClass); +registerObjectType('CLAS', ClassKind, AdkClass, { endpoint: 'oo/classes' }); diff --git a/packages/adk/src/objects/repository/devc/devc.model.ts b/packages/adk/src/objects/repository/devc/devc.model.ts index 9d528dff..a6e7d45e 100644 --- a/packages/adk/src/objects/repository/devc/devc.model.ts +++ b/packages/adk/src/objects/repository/devc/devc.model.ts @@ -162,7 +162,7 @@ export class AdkPackage // Return as AbapObject array return objRefs.map((ref) => ({ - type: ref.type?.split('/')[0] ?? '', // Extract main type from "CLAS/OC" -> "CLAS" + type: ref.type ?? '', name: ref.name, description: ref.description ?? '', uri: ref.uri ?? '', @@ -264,12 +264,29 @@ export class AdkPackage ): Promise { const context = ctx ?? getGlobalContext(); const pkg = new AdkPackage(context, name); - // Merge provided data with defaults + // SAP requires ALL elements in the Package sequence, even if empty. + // The sequence is: attributes, superPackage, extensionAlias, switch, + // applicationComponent, transport, translation, useAccesses, + // packageInterfaces, subPackages. + // Missing elements cause "System expected the element ..." errors. pkg.setData({ name, type: 'DEVC/K', description: data.description ?? name, responsible: data.responsible ?? '', + attributes: { + packageType: 'development', + ...data.attributes, + }, + superPackage: data.superPackage ?? {}, + extensionAlias: data.extensionAlias ?? {}, + switch: data.switch ?? {}, + applicationComponent: data.applicationComponent ?? {}, + transport: data.transport ?? {}, + translation: data.translation ?? {}, + useAccesses: data.useAccesses ?? {}, + packageInterfaces: data.packageInterfaces ?? {}, + subPackages: data.subPackages ?? {}, ...data, } as PackageXml); await pkg.save({ transport: options?.transport, mode: 'create' }); @@ -279,4 +296,7 @@ export class AdkPackage // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('DEVC', PackageKind, AdkPackage); +registerObjectType('DEVC', PackageKind, AdkPackage, { + endpoint: 'packages', + nameTransform: 'preserve', +}); diff --git a/packages/adk/src/objects/repository/fugr/fugr.model.ts b/packages/adk/src/objects/repository/fugr/fugr.model.ts index 5b382d14..96845605 100644 --- a/packages/adk/src/objects/repository/fugr/fugr.model.ts +++ b/packages/adk/src/objects/repository/fugr/fugr.model.ts @@ -79,4 +79,6 @@ export class AdkFunctionGroup extends AdkMainObject< // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('FUGR', FunctionGroupKind, AdkFunctionGroup); +registerObjectType('FUGR', FunctionGroupKind, AdkFunctionGroup, { + endpoint: 'functions/groups', +}); diff --git a/packages/adk/src/objects/repository/intf/intf.model.ts b/packages/adk/src/objects/repository/intf/intf.model.ts index 0063c7a2..63463663 100644 --- a/packages/adk/src/objects/repository/intf/intf.model.ts +++ b/packages/adk/src/objects/repository/intf/intf.model.ts @@ -76,4 +76,6 @@ export class AdkInterface extends AdkMainObject< // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('INTF', InterfaceKind, AdkInterface); +registerObjectType('INTF', InterfaceKind, AdkInterface, { + endpoint: 'oo/interfaces', +}); diff --git a/packages/adk/src/objects/repository/prog/prog.model.ts b/packages/adk/src/objects/repository/prog/prog.model.ts index 12e1082a..3f6d123c 100644 --- a/packages/adk/src/objects/repository/prog/prog.model.ts +++ b/packages/adk/src/objects/repository/prog/prog.model.ts @@ -76,4 +76,6 @@ export class AdkProgram extends AdkMainObject { // Self-register with ADK registry import { registerObjectType } from '../../../base/registry'; -registerObjectType('PROG', ProgramKind, AdkProgram); +registerObjectType('PROG', ProgramKind, AdkProgram, { + endpoint: 'programs/programs', +}); diff --git a/packages/adt-cli/src/lib/cli.ts b/packages/adt-cli/src/lib/cli.ts index beb749b5..bbe04384 100644 --- a/packages/adt-cli/src/lib/cli.ts +++ b/packages/adt-cli/src/lib/cli.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { + importObjectCommand, importPackageCommand, importTransportCommand, searchCommand, @@ -19,6 +20,8 @@ import { createReplCommand, packageGetCommand, lsCommand, + unlockCommand, + checkCommand, } from './commands'; import { refreshCommand } from './commands/auth/refresh'; // Deploy command moved to @abapify/adt-export plugin @@ -190,6 +193,7 @@ export async function createCLI(options?: { .command('import') .description('Import ABAP objects to various formats (abapGit, etc.)'); + importCmd.addCommand(importObjectCommand); importCmd.addCommand(importPackageCommand); importCmd.addCommand(importTransportCommand); @@ -199,6 +203,12 @@ export async function createCLI(options?: { // Deploy command moved to @abapify/adt-export plugin // Add '@abapify/adt-export/commands/export' to adt.config.ts commands array to enable + // Unlock command (force-release stale locks) + program.addCommand(unlockCommand); + + // Check command (syntax check / checkruns) + program.addCommand(checkCommand); + // REPL - Interactive hypermedia navigator program.addCommand(createReplCommand()); diff --git a/packages/adt-cli/src/lib/commands/check.ts b/packages/adt-cli/src/lib/commands/check.ts new file mode 100644 index 00000000..0e2577e0 --- /dev/null +++ b/packages/adt-cli/src/lib/commands/check.ts @@ -0,0 +1,437 @@ +/** + * Check Command + * + * Run SAP syntax check (checkruns) on ABAP objects. + * Supports checking individual objects, all objects in a package, + * or all objects in a transport request. + * + * This is NOT ATC — it's the basic syntax/check run endpoint + * at /sap/bc/adt/checkruns. + * + * Usage: + * adt check ZAGE_CHAR_WITH_LENGTH # Single object (auto-resolves URI) + * adt check --package ZABAPGIT_EXAMPLES # All objects in package + * adt check --transport DEVK900001 # All objects in transport + */ + +import { Command } from 'commander'; +import { XMLParser } from 'fast-xml-parser'; +import { getAdtClientV2 } from '../utils/adt-client-v2'; +import { getObjectUri } from '@abapify/adk'; + +type SearchObject = { + name?: string; + type?: string; + uri?: string; + description?: string; + packageName?: string; +}; + +type CheckMessage = { + uri?: string; + type?: string; + shortText?: string; + category?: string; + code?: string; +}; + +type CheckReport = { + checkMessageList?: { + checkMessage?: CheckMessage | CheckMessage[]; + }; + reporter?: string; + triggeringUri?: string; + status?: string; + statusText?: string; +}; + +/** + * Resolve a single object name to its ADT URI via quickSearch + */ +async function resolveObjectUri( + client: Awaited>, + objectName: string, + typeHint?: string, +): Promise<{ uri: string; type: string; name: string }> { + // Type hint → construct URI from ADK registry + if (typeHint) { + const uri = getObjectUri(typeHint, objectName); + if (uri) { + return { + uri, + type: typeHint.toUpperCase(), + name: objectName.toUpperCase(), + }; + } + } + + // Search-based resolution + const searchResult = + await client.adt.repository.informationsystem.search.quickSearch({ + query: objectName, + maxResults: 10, + }); + + const resultsAny = searchResult as Record; + let rawObjects: SearchObject | SearchObject[] | undefined; + if ('objectReferences' in resultsAny && resultsAny.objectReferences) { + const refs = resultsAny.objectReferences as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = refs.objectReference; + } else if ('mainObject' in resultsAny && resultsAny.mainObject) { + const main = resultsAny.mainObject as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = main.objectReference; + } + + const objects: SearchObject[] = rawObjects + ? Array.isArray(rawObjects) + ? rawObjects + : [rawObjects] + : []; + + // Find exact match + const match = objects.find( + (o) => o.name?.toUpperCase() === objectName.toUpperCase(), + ); + + if (!match?.uri) { + throw new Error(`Object '${objectName}' not found`); + } + + return { + uri: match.uri, + type: match.type ?? 'UNKNOWN', + name: match.name ?? objectName, + }; +} + +/** + * Search objects by package using quickSearch with package filter + */ +async function resolvePackageObjects( + client: Awaited>, + packageName: string, +): Promise> { + const searchResult = + await client.adt.repository.informationsystem.search.quickSearch({ + query: `*`, + maxResults: 200, + objectType: undefined as unknown as string, + packageName, + }); + + const resultsAny = searchResult as Record; + let rawObjects: SearchObject | SearchObject[] | undefined; + if ('objectReferences' in resultsAny && resultsAny.objectReferences) { + const refs = resultsAny.objectReferences as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = refs.objectReference; + } else if ('mainObject' in resultsAny && resultsAny.mainObject) { + const main = resultsAny.mainObject as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = main.objectReference; + } + + const objects: SearchObject[] = rawObjects + ? Array.isArray(rawObjects) + ? rawObjects + : [rawObjects] + : []; + + return objects + .filter((o) => o.uri) + .map((o) => ({ + uri: o.uri!, + type: o.type ?? 'UNKNOWN', + name: o.name ?? '', + })); +} + +/** + * Build checkObjectList XML for the checkruns endpoint + */ +function buildCheckObjectListXml( + objects: Array<{ uri: string }>, + version: string = 'active', +): string { + const checkObjects = objects + .map( + (o) => + ` `, + ) + .join('\n'); + + return ` + +${checkObjects} +`; +} + +/** + * Parse XML response from checkruns endpoint + */ +function parseCheckRunXml(xmlOrParsed: unknown): { + reports: CheckReport[]; + hasErrors: boolean; + hasWarnings: boolean; +} { + let data: Record; + + if (typeof xmlOrParsed === 'string') { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '', + removeNSPrefix: true, + }); + data = parser.parse(xmlOrParsed) as Record; + } else { + data = xmlOrParsed as Record; + } + + let reports: CheckReport[] = []; + let hasErrors = false; + let hasWarnings = false; + + // Navigate into checkRunReports > checkReport + const reportsRoot = (data.checkRunReports ?? data) as Record; + const rawReports = reportsRoot.checkReport; + if (rawReports) { + const arr = Array.isArray(rawReports) ? rawReports : [rawReports]; + reports = arr.map((r: Record) => ({ + reporter: (r.reporter as string) ?? undefined, + triggeringUri: (r.triggeringUri as string) ?? undefined, + status: (r.status as string) ?? undefined, + statusText: (r.statusText as string) ?? undefined, + checkMessageList: r.checkMessageList as CheckReport['checkMessageList'], + })); + } + + for (const report of reports) { + const msgList = report.checkMessageList; + if (!msgList?.checkMessage) continue; + + const messages = Array.isArray(msgList.checkMessage) + ? msgList.checkMessage + : [msgList.checkMessage]; + + for (const msg of messages) { + const sev = msg.type ?? msg.category; + if (sev === 'E' || sev === 'A') hasErrors = true; + if (sev === 'W') hasWarnings = true; + } + } + + return { reports, hasErrors, hasWarnings }; +} + +/** + * Display check results + */ +function displayResults(reports: CheckReport[]): number { + let totalMessages = 0; + + for (const report of reports) { + const msgList = report.checkMessageList; + if (!msgList?.checkMessage) { + // No messages — clean + if (report.triggeringUri) { + const objName = + report.triggeringUri.split('/').pop() ?? report.triggeringUri; + console.log(` ✅ ${objName}`); + } + continue; + } + + const messages = Array.isArray(msgList.checkMessage) + ? msgList.checkMessage + : [msgList.checkMessage]; + + if (messages.length === 0) continue; + + const objName = + report.triggeringUri?.split('/').pop() ?? report.reporter ?? 'unknown'; + + for (const msg of messages) { + totalMessages++; + const sev = msg.type ?? msg.category; + const icon = + sev === 'E' || sev === 'A' ? '❌' : sev === 'W' ? '⚠️' : 'ℹ️'; + console.log( + ` ${icon} ${objName}: ${msg.shortText ?? msg.code ?? 'unknown message'}`, + ); + } + } + + return totalMessages; +} + +export const checkCommand = new Command('check') + .description('Run syntax check (checkruns) on ABAP objects') + .argument('[objects...]', 'Object name(s) to check') + .option('-p, --package ', 'Check all objects in a package') + .option( + '-t, --transport ', + 'Check all objects in a transport request', + ) + .option( + '--type ', + 'Object type hint for resolving URIs (e.g., CLAS, DOMA)', + ) + .option( + '--version ', + 'Version to check: active, inactive, new', + 'new', + ) + .option('--json', 'Output results as JSON') + .action( + async ( + objects: string[], + options: { + package?: string; + transport?: string; + type?: string; + version?: string; + json?: boolean; + }, + ) => { + try { + const client = await getAdtClientV2(); + const checkObjects: Array<{ uri: string; type: string; name: string }> = + []; + + // Mode 1: Package + if (options.package) { + console.log(`🔍 Resolving objects in package ${options.package}...`); + const pkgObjects = await resolvePackageObjects( + client, + options.package, + ); + if (pkgObjects.length === 0) { + console.log(`⚠️ No objects found in package ${options.package}`); + return; + } + checkObjects.push(...pkgObjects); + console.log(` Found ${checkObjects.length} object(s)`); + } + // Mode 2: Transport (fetch objects from transport tasks) + else if (options.transport) { + console.log( + `🔍 Resolving objects in transport ${options.transport}...`, + ); + const trResult = await client.fetch( + `/sap/bc/adt/cts/transportrequests/${options.transport}`, + { + method: 'GET', + headers: { + Accept: 'application/vnd.sap.adt.transportrequests.v1+xml', + }, + }, + ); + // Parse transport objects from response + const trData = trResult as Record; + // Transport response contains objects — extract URIs + // For now, search by transport number as a workaround + console.log(`⚠️ Transport object extraction: parsing response...`); + // Attempt to find object references in the transport data + const trJson = JSON.stringify(trData); + const uriMatches = trJson.match(/\/sap\/bc\/adt\/[^"]+/g); + if (uriMatches && uriMatches.length > 0) { + const uniqueUris = [...new Set(uriMatches)].filter( + (u) => !u.includes('transportrequests') && !u.includes('cts/'), + ); + for (const uri of uniqueUris) { + const name = uri.split('/').pop() ?? ''; + checkObjects.push({ uri, type: 'UNKNOWN', name }); + } + } + if (checkObjects.length === 0) { + console.log( + `⚠️ No objects found in transport ${options.transport}`, + ); + return; + } + console.log(` Found ${checkObjects.length} object(s)`); + } + // Mode 3: Individual objects + else if (objects.length > 0) { + console.log(`🔍 Resolving ${objects.length} object(s)...`); + for (const objectName of objects) { + try { + const resolved = await resolveObjectUri( + client, + objectName, + options.type, + ); + checkObjects.push(resolved); + console.log( + ` 📄 ${resolved.name} (${resolved.type}) → ${resolved.uri}`, + ); + } catch (err) { + console.error( + ` ❌ ${objectName}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + } else { + console.error('❌ Specify object name(s), --package, or --transport'); + process.exit(1); + } + + if (checkObjects.length === 0) { + console.error('❌ No objects to check'); + process.exit(1); + } + + // Build and POST checkrun request + const xml = buildCheckObjectListXml(checkObjects, options.version); + console.log( + `\n🔄 Running syntax check on ${checkObjects.length} object(s)...`, + ); + + const response = await client.fetch('/sap/bc/adt/checkruns', { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.sap.adt.checkobjects+xml', + Accept: 'application/vnd.sap.adt.checkmessages+xml', + }, + body: xml, + }); + + // Parse and display results + const { reports, hasErrors, hasWarnings } = parseCheckRunXml(response); + + if (options.json) { + console.log(JSON.stringify(reports, null, 2)); + } else { + console.log(`\n📋 Check Results:`); + const totalMessages = displayResults(reports); + + if (totalMessages === 0) { + console.log( + `\n✅ All ${checkObjects.length} object(s) passed syntax check`, + ); + } else { + console.log(`\n📊 ${totalMessages} message(s) found`); + if (hasErrors) { + console.log('❌ Errors detected'); + process.exit(1); + } + if (hasWarnings) { + console.log('⚠️ Warnings detected'); + } + } + } + } catch (error) { + console.error( + '❌ Check failed:', + error instanceof Error ? error.message : String(error), + ); + process.exit(1); + } + }, + ); diff --git a/packages/adt-cli/src/lib/commands/import/object.ts b/packages/adt-cli/src/lib/commands/import/object.ts new file mode 100644 index 00000000..70150f66 --- /dev/null +++ b/packages/adt-cli/src/lib/commands/import/object.ts @@ -0,0 +1,84 @@ +import { Command } from 'commander'; +import { ImportService } from '../../services/import/service'; +import { IconRegistry } from '../../utils/icon-registry'; +import { getAdtClientV2 } from '../../utils/adt-client-v2'; + +export const importObjectCommand = new Command('object') + .argument( + '', + 'ABAP object name to import (e.g., ZAGE_DOMA_CASE_SENSITIVE)', + ) + .argument('[targetFolder]', 'Target folder for output') + .description( + 'Import a single ABAP object by name (searches, resolves type, and creates local file)', + ) + .option( + '-o, --output ', + 'Output directory (overrides targetFolder)', + '', + ) + .option( + '--format ', + 'Output format: abapgit | @abapify/adt-plugin-abapgit', + 'abapgit', + ) + .option( + '--format-option ', + 'Format-specific option (repeatable), e.g. --format-option folderLogic=full', + (value: string, previous: string[]) => [...previous, value], + [], + ) + .option('--debug', 'Enable debug output', false) + .action(async (objectName, targetFolder, options) => { + try { + // Initialize ADT client (also initializes ADK) + await getAdtClientV2(); + + const importService = new ImportService(); + + // Determine output path: --output option, targetFolder argument, or cwd + const outputPath = options.output || targetFolder || './src'; + + console.log(`🔍 Searching for object: ${objectName}`); + + // Parse format options + const formatOptions: Record = {}; + for (const entry of options.formatOption ?? []) { + const separatorIndex = entry.indexOf('='); + if (separatorIndex <= 0 || separatorIndex === entry.length - 1) { + throw new Error( + `Invalid --format-option '${entry}'. Expected key=value format.`, + ); + } + formatOptions[entry.slice(0, separatorIndex).trim()] = entry + .slice(separatorIndex + 1) + .trim(); + } + + const result = await importService.importObject({ + objectName, + outputPath, + format: options.format, + formatOptions, + debug: options.debug, + }); + + // Display results + if (result.results.success > 0) { + const icon = IconRegistry.getIcon(result.objectType || ''); + console.log( + `\n${icon} ${result.objectType} ${result.objectName}: imported`, + ); + console.log(`✨ Files written to: ${result.outputPath}`); + } else { + console.log(`\n❌ Failed to import ${objectName}`); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.error(`❌ ${msg}`); + if (options.debug && error instanceof Error && error.stack) { + console.error(error.stack); + } + process.exit(1); + } + }); diff --git a/packages/adt-cli/src/lib/commands/index.ts b/packages/adt-cli/src/lib/commands/index.ts index 91fe2fa0..41c95f67 100644 --- a/packages/adt-cli/src/lib/commands/index.ts +++ b/packages/adt-cli/src/lib/commands/index.ts @@ -1,4 +1,5 @@ // Export all commands directly +export { importObjectCommand } from './import/object'; export { importPackageCommand } from './import/package'; export { importTransportCommand } from './import/transport'; // Export commands moved to @abapify/adt-export plugin @@ -19,3 +20,5 @@ export { createCtsCommand } from './cts'; export { createReplCommand } from './repl'; export { packageGetCommand } from './package'; export { lsCommand } from './ls'; +export { unlockCommand } from './unlock'; +export { checkCommand } from './check'; diff --git a/packages/adt-cli/src/lib/commands/unlock.ts b/packages/adt-cli/src/lib/commands/unlock.ts new file mode 100644 index 00000000..8dafcb38 --- /dev/null +++ b/packages/adt-cli/src/lib/commands/unlock.ts @@ -0,0 +1,165 @@ +/** + * Unlock Command + * + * Force-unlock SAP objects that are stuck in locked state + * (e.g., from a crashed session or failed deploy). + * + * Uses _action=UNLOCK without a lock handle, which SAP allows + * for locks owned by the current user/session. + */ + +import { Command } from 'commander'; +import { getAdtClientV2 } from '../utils/adt-client-v2'; +import { getObjectUri, getRegisteredTypes } from '@abapify/adk'; + +type SearchObject = { + name?: string; + type?: string; + uri?: string; + description?: string; + packageName?: string; +}; + +async function resolveObjectUri( + client: Awaited>, + objectName: string, + typeHint?: string, + uriOverride?: string, +): Promise<{ uri: string; display: string }> { + // Direct URI override + if (uriOverride) { + return { uri: uriOverride, display: `${objectName} (via --uri)` }; + } + + // Type hint → construct URI from ADK registry + if (typeHint) { + const uri = getObjectUri(typeHint, objectName); + if (uri) { + return { uri, display: `${objectName} (${typeHint.toUpperCase()})` }; + } + console.warn( + `⚠️ Type '${typeHint}' has no registered endpoint. Falling back to search...`, + ); + } + + // Search-based resolution + const searchResult = + await client.adt.repository.informationsystem.search.quickSearch({ + query: objectName, + maxResults: 10, + }); + + const rawObjects = + 'objectReference' in searchResult ? searchResult.objectReference : []; + const objects: SearchObject[] = Array.isArray(rawObjects) + ? rawObjects + : rawObjects + ? [rawObjects] + : []; + + const exactMatch = objects.find( + (obj) => String(obj.name || '').toUpperCase() === objectName.toUpperCase(), + ); + + if (!exactMatch?.uri) { + const typeList = getRegisteredTypes().join(', '); + throw new Error( + `Object '${objectName}' not found via search.\n` + + `💡 Try specifying the type: adt unlock ${objectName} --type TTYP\n` + + ` Registered types: ${typeList}\n` + + ` Or provide a direct URI: adt unlock ${objectName} --uri /sap/bc/adt/...`, + ); + } + + return { + uri: exactMatch.uri, + display: `${exactMatch.name} (${exactMatch.type}) - ${exactMatch.description ?? ''}`, + }; +} + +async function performUnlock( + client: Awaited>, + objectUri: string, + objectName: string, + lockHandle?: string, +): Promise { + const query = lockHandle + ? `?_action=UNLOCK&lockHandle=${lockHandle}` + : '?_action=UNLOCK'; + + try { + await client.fetch(`${objectUri}${query}`, { + method: 'POST', + headers: { + 'X-sap-adt-sessiontype': 'stateful', + }, + }); + console.log(`✅ ${objectName} unlocked`); + } catch (unlockError: unknown) { + const msg = + unlockError instanceof Error ? unlockError.message : String(unlockError); + + if (msg.includes('not locked') || msg.includes('not enqueued')) { + console.log(`ℹ️ ${objectName} is not locked`); + } else { + throw new Error(`Unlock failed for ${objectName}: ${msg}`); + } + } +} + +export const unlockCommand = new Command('unlock') + .description('Unlock SAP objects (force-release stale locks)') + .argument( + '', + 'Object name(s) to unlock (e.g., ZAGE_STRING_TABLE ZAGE_STRUCT_TABLE_TYPE)', + ) + .option('--lock-handle ', 'Specific lock handle (if known)') + .option( + '--type ', + 'Object type (CLAS, INTF, TTYP, TABL, DOMA, DTEL, PROG, FUGR, DEVC)', + ) + .option('--uri ', 'Direct object URI (skips search)') + .action( + async ( + objectNames: string[], + options: { lockHandle?: string; type?: string; uri?: string }, + ) => { + try { + const client = await getAdtClientV2(); + let failed = 0; + + for (const objectName of objectNames) { + console.log(`\n🔓 Unlocking: ${objectName}`); + + try { + const { uri, display } = await resolveObjectUri( + client, + objectName, + options.type, + // URI override only makes sense for single object + objectNames.length === 1 ? options.uri : undefined, + ); + console.log(` 📍 ${display}`); + + await performUnlock(client, uri, objectName, options.lockHandle); + } catch (err) { + console.error( + ` ❌ ${err instanceof Error ? err.message : String(err)}`, + ); + failed++; + } + } + + if (failed > 0) { + console.log(`\n⚠️ ${failed}/${objectNames.length} unlock(s) failed`); + process.exit(1); + } + } catch (error) { + console.error( + '❌ Failed:', + error instanceof Error ? error.message : String(error), + ); + process.exit(1); + } + }, + ); diff --git a/packages/adt-cli/src/lib/plugin-loader.ts b/packages/adt-cli/src/lib/plugin-loader.ts index 74de55d2..d14848c5 100644 --- a/packages/adt-cli/src/lib/plugin-loader.ts +++ b/packages/adt-cli/src/lib/plugin-loader.ts @@ -85,6 +85,11 @@ function pluginToCommand( ): Command { const cmd = new Command(plugin.name).description(plugin.description); + // Add alias if specified + if (plugin.alias) { + cmd.alias(plugin.alias); + } + // Add options if (plugin.options) { for (const opt of plugin.options) { @@ -129,7 +134,7 @@ function pluginToCommand( const args: Record = { ...options }; if (plugin.arguments) { plugin.arguments.forEach((argDef, index) => { - const argName = argDef.name.replace(/[<>[\]]/g, ''); + const argName = argDef.name.replace(/[<>[\].]/g, ''); args[argName] = actionArgs[index]; }); } diff --git a/packages/adt-cli/src/lib/services/import/service.ts b/packages/adt-cli/src/lib/services/import/service.ts index dd361a6d..c7b2029b 100644 --- a/packages/adt-cli/src/lib/services/import/service.ts +++ b/packages/adt-cli/src/lib/services/import/service.ts @@ -33,6 +33,22 @@ async function resolvePackagePath(packageName: string): Promise { return path; } +/** + * Options for importing a single object by name (search-based) + */ +export interface ObjectImportOptions { + /** Object name to search for (e.g., 'ZAGE_DOMA_CASE_SENSITIVE') */ + objectName: string; + /** Output directory for serialized files */ + outputPath: string; + /** Format plugin name or package (e.g., 'abapgit', '@abapify/adt-plugin-abapgit') */ + format: string; + /** Format-specific options provided via CLI */ + formatOptions?: Record; + /** Enable debug output */ + debug?: boolean; +} + /** * Options for importing a transport request */ @@ -77,6 +93,10 @@ export interface ImportResult { transportNumber?: string; /** Package name (for package imports) */ packageName?: string; + /** Object name (for single-object imports) */ + objectName?: string; + /** Object type (for single-object imports) */ + objectType?: string; /** Description of the imported content */ description: string; /** Total objects found */ @@ -317,8 +337,9 @@ export class ImportService { let objectsToImport = allObjects; if (options.objectTypes && options.objectTypes.length > 0) { const types = options.objectTypes.map((t) => t.toUpperCase()); - objectsToImport = allObjects.filter((obj: { type: string }) => - types.includes(obj.type), + objectsToImport = allObjects.filter( + (obj: { type: string }) => + types.includes(obj.type) || types.includes(obj.type.split('/')[0]), ); if (options.debug) { console.log( @@ -412,4 +433,174 @@ export class ImportService { outputPath: options.outputPath, }; } + + /** + * Import a single object by name (search-based resolution) + * + * Flow: + * 1. quickSearch for the object name + * 2. Find exact match — if ambiguous, report all matches + * 3. Extract ADT type from search result + * 4. Load via ADK factory + format plugin → local file + * + * @param options - Import options including object name, output path, and format + * @returns Import result with statistics + */ + async importObject(options: ObjectImportOptions): Promise { + if (options.debug) { + console.log(`🔍 Searching for object: ${options.objectName}`); + console.log(`📁 Output path: ${options.outputPath}`); + console.log(`🎯 Format: ${options.format}`); + } + + // Step 1: Search for the object via quickSearch + const ctx = getGlobalContext(); + const searchResult = + await ctx.client.adt.repository.informationsystem.search.quickSearch({ + query: options.objectName, + maxResults: 10, + }); + + type SearchObject = { + name?: string; + type?: string; + uri?: string; + description?: string; + packageName?: string; + }; + + // Handle different response shapes from quickSearch + const resultsAny = searchResult as Record; + if (options.debug) { + console.log( + `🔎 Raw search result keys: ${Object.keys(resultsAny).join(', ')}`, + ); + } + let rawObjects: SearchObject | SearchObject[] | undefined; + if ('objectReference' in resultsAny && resultsAny.objectReference) { + rawObjects = resultsAny.objectReference as SearchObject | SearchObject[]; + } else if ( + 'objectReferences' in resultsAny && + resultsAny.objectReferences + ) { + const refs = resultsAny.objectReferences as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = refs.objectReference; + } else if ('mainObject' in resultsAny && resultsAny.mainObject) { + const main = resultsAny.mainObject as { + objectReference?: SearchObject | SearchObject[]; + }; + rawObjects = main.objectReference; + } + const objects: SearchObject[] = rawObjects + ? Array.isArray(rawObjects) + ? rawObjects + : [rawObjects] + : []; + + // Step 2: Find exact match (case-insensitive) + const exactMatch = objects.find( + (obj: SearchObject) => + String(obj.name || '').toUpperCase() === + options.objectName.toUpperCase(), + ); + + if (!exactMatch) { + // Show similar objects as a hint + const similar = objects + .filter((obj: SearchObject) => + String(obj.name || '') + .toUpperCase() + .includes(options.objectName.toUpperCase()), + ) + .slice(0, 5); + + const hint = + similar.length > 0 + ? `\n💡 Similar objects:\n${similar.map((o: SearchObject) => ` • ${o.name} (${o.type}) – ${o.packageName}`).join('\n')}` + : ''; + + throw new Error( + `Object '${options.objectName}' not found in the system.${hint}`, + ); + } + + // Extract base type (e.g. "DOMA/DD" → "DOMA") + const fullType = String(exactMatch.type || ''); + const slashIndex = fullType.indexOf('/'); + const baseType = + slashIndex >= 0 ? fullType.substring(0, slashIndex) : fullType; + + if (options.debug) { + console.log( + `✅ Resolved: ${exactMatch.name} (${baseType}) in package ${exactMatch.packageName}`, + ); + } + + // Step 3: Load format plugin + const plugin = await loadFormatPlugin(options.format); + const configFormatOptions = await this.getConfigFormatOptions( + options.format, + ); + + if (!plugin.instance.registry.isSupported(baseType)) { + throw new Error( + `Object type '${baseType}' is not supported by format plugin '${plugin.name}'.`, + ); + } + + // Step 4: Load ADK object via factory + const factory = createAdkFactory(ctx); + const adkObject = factory.get(String(exactMatch.name), baseType); + await (adkObject as any).load(); + + // Step 5: Serialize via format plugin + const context: ImportContext = { + resolvePackagePath, + formatOptions: options.formatOptions, + configFormatOptions, + }; + + const results = { success: 0, skipped: 0, failed: 0 }; + const objectsByType: Record = {}; + + const result = await plugin.instance.format.import( + adkObject as any, + options.outputPath, + context, + ); + + if (result.success) { + objectsByType[baseType] = 1; + results.success = 1; + if (options.debug) { + console.log(` ✅ ${baseType} ${exactMatch.name}`); + } + } else { + results.failed = 1; + if (options.debug) { + console.log( + ` ❌ ${baseType} ${exactMatch.name}: ${result.errors?.join(', ') || 'unknown error'}`, + ); + } + } + + // Call afterImport hook if available + if (plugin.instance.hooks?.afterImport) { + await plugin.instance.hooks.afterImport(options.outputPath); + } + + return { + objectName: String(exactMatch.name), + objectType: baseType, + description: + String(exactMatch.description || '') || + `${baseType} ${exactMatch.name}`, + totalObjects: 1, + results, + objectsByType, + outputPath: options.outputPath, + }; + } } diff --git a/packages/adt-cli/src/lib/utils/object-uri.ts b/packages/adt-cli/src/lib/utils/object-uri.ts index 33542e2f..9deaf9b5 100644 --- a/packages/adt-cli/src/lib/utils/object-uri.ts +++ b/packages/adt-cli/src/lib/utils/object-uri.ts @@ -1,9 +1,13 @@ import { basename } from 'path'; +import { + getEndpointForType, + getObjectUri as adkGetObjectUri, +} from '@abapify/adk'; export interface ObjectTypeMapping { endpoint: string; description: string; - sections?: Record; // Optional section mappings + sections?: Record; } export interface ParsedAbapFile { @@ -20,70 +24,35 @@ export interface ObjectTypeInfo { section?: string; } -// Map ABAP object types to SAP ADT endpoints -// Based on ABAP file format naming convention: ..[
].abap -const ABAP_OBJECT_MAPPINGS: Record = { - // Object-Oriented Programming +/** + * Section mappings for object types that have sub-sources. + * Endpoints come from the ADK registry; this only holds section info. + */ +const SECTION_MAPPINGS: Record> = { clas: { - endpoint: 'oo/classes', - description: 'Class', - sections: { - // Main class file (no section) - '': 'source/main', - // Class sections - definitions: 'source/definitions', - implementations: 'source/implementations', - macros: 'source/macros', - testclasses: 'source/testclasses', - }, - }, - intf: { - endpoint: 'oo/interfaces', - description: 'Interface', - // Interfaces typically don't have sections - }, - - // Programs and Includes - prog: { - endpoint: 'programs/programs', - description: 'Program', - }, - incl: { - endpoint: 'programs/includes', - description: 'Include', - }, - - // Function Groups - fugr: { - endpoint: 'functions/groups', - description: 'Function Group', - }, - - // Data Dictionary - dtel: { - endpoint: 'ddic/dataelements', - description: 'Data Element', - }, - doma: { - endpoint: 'ddic/domains', - description: 'Domain', - }, - tabl: { - endpoint: 'ddic/tables', - description: 'Table', - }, - ttyp: { - endpoint: 'ddic/tabletypes', - description: 'Table Type', - }, - - // Transformations and Web Services - xslt: { - endpoint: 'transformations', - description: 'XSLT Transformation', + '': 'source/main', + definitions: 'source/definitions', + implementations: 'source/implementations', + macros: 'source/macros', + testclasses: 'source/testclasses', }, +}; - // Additional object types can be easily added here +/** + * Human-readable descriptions for object types. + * Kept here since ADK registry doesn't carry descriptions. + */ +const TYPE_DESCRIPTIONS: Record = { + clas: 'Class', + intf: 'Interface', + prog: 'Program', + incl: 'Include', + fugr: 'Function Group', + dtel: 'Data Element', + doma: 'Domain', + tabl: 'Table', + ttyp: 'Table Type', + xslt: 'XSLT Transformation', }; /** @@ -117,7 +86,7 @@ export function parseAbapFilename(filename: string): ParsedAbapFile | null { /** * Generic function to detect ABAP object type information from filename - * Uses the parsed filename structure and object mappings + * Uses the parsed filename structure and ADK registry for endpoint resolution */ export function detectObjectTypeFromFilename( filename: string, @@ -127,16 +96,17 @@ export function detectObjectTypeFromFilename( return null; } - const mapping = ABAP_OBJECT_MAPPINGS[parsed.type]; - if (!mapping) { + const adtType = parsed.type.toUpperCase(); + const endpoint = getEndpointForType(adtType); + if (!endpoint) { return null; } return { - type: parsed.type.toUpperCase(), + type: adtType, name: parsed.name.toUpperCase(), - endpoint: mapping.endpoint, - description: mapping.description, + endpoint, + description: TYPE_DESCRIPTIONS[parsed.type] ?? adtType, section: parsed.section, }; } @@ -146,7 +116,10 @@ export function detectObjectTypeFromFilename( * Returns the base URI for object operations (without source path) */ export function objectInfoToUri(objectInfo: ObjectTypeInfo): string { - return `/sap/bc/adt/${objectInfo.endpoint}/${objectInfo.name.toLowerCase()}`; + return ( + adkGetObjectUri(objectInfo.type, objectInfo.name) ?? + `/sap/bc/adt/${objectInfo.endpoint}/${objectInfo.name.toLowerCase()}` + ); } /** @@ -166,11 +139,11 @@ export function getSourcePath( objectInfo: ObjectTypeInfo, version?: 'active' | 'inactive', ): string { - const mapping = ABAP_OBJECT_MAPPINGS[objectInfo.type.toLowerCase()]; + const sections = SECTION_MAPPINGS[objectInfo.type.toLowerCase()]; // Check if object type has section mappings and if section is provided - if (mapping?.sections && objectInfo.section) { - const sectionPath = mapping.sections[objectInfo.section]; + if (sections && objectInfo.section) { + const sectionPath = sections[objectInfo.section]; if (sectionPath) { return version ? `${sectionPath}?version=${version}` : sectionPath; } diff --git a/packages/adt-export/package.json b/packages/adt-export/package.json index 51f548e8..e5321547 100644 --- a/packages/adt-export/package.json +++ b/packages/adt-export/package.json @@ -11,6 +11,8 @@ "exports": { ".": "./dist/index.mjs", "./commands/export": "./dist/commands/export.mjs", + "./commands/roundtrip": "./dist/commands/roundtrip.mjs", + "./commands/activate": "./dist/commands/activate.mjs", "./package.json": "./package.json" }, "files": [ @@ -26,9 +28,12 @@ "abapgit" ], "dependencies": { - "@abapify/adt-plugin": "^0.1.7", "@abapify/adk": "^0.1.7", - "@abapify/adt-plugin-abapgit": "^0.1.7" + "@abapify/adt-plugin": "^0.1.7", + "@abapify/adt-plugin-abapgit": "^0.1.7", + "chalk": "^5.6.2", + "diff": "^8.0.3", + "fast-xml-parser": "^5.5.5" }, "module": "./dist/index.mjs" } diff --git a/packages/adt-export/src/commands/activate.ts b/packages/adt-export/src/commands/activate.ts new file mode 100644 index 00000000..cb5fdc4a --- /dev/null +++ b/packages/adt-export/src/commands/activate.ts @@ -0,0 +1,292 @@ +/** + * Activate Command Plugin + * + * Activates inactive ABAP objects on SAP by file list, package, or transport. + * + * Usage: + * adt activate zage_fixed_values.doma.xml (specific files) + * adt activate *.doma.xml (glob pattern) + * adt activate -p ZABAPGIT_EXAMPLES (all objects in package) + * adt activate -t DEVK900001 (all objects in transport) + */ + +import type { + CliCommandPlugin, + CliContext, + AdtPlugin, + ExportOptions, +} from '@abapify/adt-plugin'; +import { + AdkObjectSet, + AdkPackage, + AdkTransport, + type AdkContext, + type AdkObject, + type AdtClient, + type ActivationResult, + createAdk, +} from '@abapify/adk'; +import { + createFileTree, + FilteredFileTree, + findAbapGitRoot, + resolveFilesRelativeToRoot, +} from '../utils/filetree'; +import { glob as nativeGlob } from 'node:fs/promises'; + +/** + * Format shortcuts + */ +const FORMAT_SHORTCUTS: Record = { + abapgit: '@abapify/adt-plugin-abapgit', + ag: '@abapify/adt-plugin-abapgit', +}; + +async function loadFormatPlugin(formatSpec: string): Promise { + const packageName = FORMAT_SHORTCUTS[formatSpec] ?? formatSpec; + const pluginModule = await import(packageName); + const PluginClass = + pluginModule.default || pluginModule[Object.keys(pluginModule)[0]]; + if (!PluginClass) { + throw new Error(`No plugin class found in ${packageName}`); + } + return typeof PluginClass === 'function' && PluginClass.prototype + ? new PluginClass() + : PluginClass; +} + +/** + * Expand glob patterns in a list of file arguments. + * Literal filenames (no glob characters) pass through unchanged. + */ +async function expandGlobs(patterns: string[], cwd: string): Promise { + const results: string[] = []; + for (const pattern of patterns) { + if (/[*?[\]{}]/.test(pattern)) { + for await (const match of nativeGlob(pattern, { cwd })) { + results.push(match); + } + } else { + results.push(pattern); + } + } + return results; +} + +export const activateCommand: CliCommandPlugin = { + name: 'activate', + description: + 'Activate inactive ABAP objects (by files, package, or transport)', + + arguments: [ + { + name: '[files...]', + description: + 'Specific files or glob patterns (e.g., zage_fixed_values.doma.xml, *.doma.xml)', + }, + ], + + options: [ + { + flags: '-s, --source ', + description: 'Source directory containing serialized files', + default: '.', + }, + { + flags: '-f, --format ', + description: 'Format plugin: abapgit | @abapify/adt-plugin-abapgit', + default: 'abapgit', + }, + { + flags: '-p, --package ', + description: 'Activate all objects in a package', + }, + { + flags: '-t, --transport ', + description: 'Activate all objects in a transport request', + }, + { + flags: '--types ', + description: 'Filter by object types (comma-separated)', + }, + ], + + async execute(args: Record, ctx: CliContext) { + const options = args as { + files?: string[]; + source: string; + format: string; + transport?: string; + package?: string; + types?: string; + }; + + if (!ctx.getAdtClient) { + ctx.logger.error('❌ ADT client not available. Run: adt auth login'); + process.exit(1); + } + + const client = await ctx.getAdtClient!(); + const adkContext: AdkContext = { client: client as any }; + + const objectTypeFilter = options.types + ? options.types.split(',').map((t) => t.trim().toUpperCase()) + : undefined; + + let objectSet: AdkObjectSet; + + // ── Selector: Transport ── + if (options.transport) { + ctx.logger.info( + `🔄 Activating objects from transport ${options.transport}`, + ); + + const transport = await AdkTransport.get(options.transport, adkContext); + const objRefs = objectTypeFilter + ? transport.getObjectsByType(...objectTypeFilter) + : transport.objects; + + ctx.logger.info(` 📋 Found ${objRefs.length} objects in transport`); + + const adk = createAdk(client as AdtClient); + objectSet = new AdkObjectSet(adkContext); + + for (const ref of objRefs) { + try { + const obj = adk.get(ref.name, ref.type); + if ('load' in obj && typeof obj.load === 'function') { + await (obj as AdkObject).load(); + objectSet.add(obj as AdkObject); + ctx.logger.info(` 📄 ${ref.type} ${ref.name}`); + } + } catch (err) { + ctx.logger.warn( + ` ⚠️ Skipping ${ref.type} ${ref.name}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + + // ── Selector: Package ── + } else if (options.package) { + ctx.logger.info(`🔄 Activating objects in package ${options.package}`); + + const pkg = await AdkPackage.get(options.package); + const objects = await pkg.getObjects(); + + const filtered = objectTypeFilter + ? objects.filter((o) => objectTypeFilter.includes(o.type.toUpperCase())) + : objects; + + ctx.logger.info(` 📋 Found ${filtered.length} objects in package`); + + const adk = createAdk(client as AdtClient); + objectSet = new AdkObjectSet(adkContext); + + for (const obj of filtered) { + try { + const adkObj = adk.get(obj.name, obj.type); + if ('load' in adkObj && typeof adkObj.load === 'function') { + await (adkObj as AdkObject).load(); + objectSet.add(adkObj as AdkObject); + ctx.logger.info(` 📄 ${obj.type} ${obj.name}`); + } + } catch (err) { + ctx.logger.warn( + ` ⚠️ Skipping ${obj.type} ${obj.name}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + + // ── Selector: Files ── + } else { + const expandedFiles = + options.files && options.files.length > 0 + ? await expandGlobs(options.files, ctx.cwd) + : undefined; + const specificFiles = + expandedFiles && expandedFiles.length > 0 ? expandedFiles : undefined; + + if (!specificFiles) { + ctx.logger.error( + '❌ Specify files, --package, or --transport to select objects for activation.', + ); + process.exit(1); + } + + const repoRoot = findAbapGitRoot(ctx.cwd); + if (!repoRoot) { + ctx.logger.error('❌ No .abapgit.xml found in any parent directory.'); + process.exit(1); + } + + const sourcePath = repoRoot; + ctx.logger.info('🔄 Activating objects from files'); + ctx.logger.info(`📁 Source: ${sourcePath}`); + ctx.logger.info(`📄 Files: ${specificFiles.join(', ')}`); + + let fileTree = createFileTree(sourcePath); + const relFiles = resolveFilesRelativeToRoot( + specificFiles, + ctx.cwd, + sourcePath, + ); + fileTree = new FilteredFileTree(fileTree, relFiles); + + const plugin = await loadFormatPlugin(options.format); + if (!plugin.format.export) { + ctx.logger.error(`❌ Plugin '${plugin.name}' does not support export`); + process.exit(1); + } + + const exportOptions: ExportOptions = { + rootPackage: undefined, + }; + + objectSet = await AdkObjectSet.fromGenerator( + plugin.format.export(fileTree, client, exportOptions), + adkContext, + { + filter: objectTypeFilter + ? (obj) => { + const objType = obj.type.toUpperCase(); + const objPrefix = objType.split('/')[0]; + return ( + objectTypeFilter.includes(objType) || + objectTypeFilter.includes(objPrefix) + ); + } + : undefined, + onObject: (obj) => { + ctx.logger.info(` 📄 ${obj.kind} ${obj.name}`); + }, + }, + ); + } + + // ── Activate ── + if (objectSet.isEmpty) { + ctx.logger.warn('⚠️ No objects to activate'); + return; + } + + ctx.logger.info(`\n⚡ Activating ${objectSet.size} objects...`); + + const result: ActivationResult = await objectSet.activateAll({ + onProgress: (msg) => ctx.logger.info(` ${msg}`), + }); + + // ── Summary ── + if (result.success > 0) { + ctx.logger.info(`✅ ${result.success} objects activated`); + } + if (result.failed > 0) { + ctx.logger.error(`❌ ${result.failed} objects failed activation`); + for (const msg of result.messages) { + ctx.logger.error(` ${msg}`); + } + process.exit(1); + } + }, +}; + +export default activateCommand; diff --git a/packages/adt-export/src/commands/export.ts b/packages/adt-export/src/commands/export.ts index 669c3278..f1af236a 100644 --- a/packages/adt-export/src/commands/export.ts +++ b/packages/adt-export/src/commands/export.ts @@ -13,7 +13,12 @@ import type { } from '@abapify/adt-plugin'; import { AdkObjectSet, AdkPackage, type AdkContext } from '@abapify/adk'; import type { ExportResult } from '../types'; -import { createFileTree } from '../utils/filetree'; +import { + createFileTree, + FilteredFileTree, + findAbapGitRoot, + resolveFilesRelativeToRoot, +} from '../utils/filetree'; /** * Format shortcuts - map short names to actual package names @@ -108,8 +113,17 @@ function displayExportResults( */ export const exportCommand: CliCommandPlugin = { name: 'export', + alias: 'deploy', description: 'Export local files to SAP (deploy serialized objects)', + arguments: [ + { + name: '[files...]', + description: + 'Specific files to deploy (e.g., zage_fixed_values.doma.xml). Omit to deploy all.', + }, + ], + options: [ { flags: '-s, --source ', @@ -139,8 +153,14 @@ export const exportCommand: CliCommandPlugin = { default: false, }, { - flags: '--no-activate', - description: 'Save inactive (skip activation)', + flags: '--activate', + description: 'Activate objects after deploy (use --no-activate to skip)', + default: true, + }, + { + flags: '--unlock', + description: + 'Force-unlock objects locked by current user before saving (auto-retry on 403)', default: false, }, { @@ -152,6 +172,7 @@ export const exportCommand: CliCommandPlugin = { async execute(args: Record, ctx: CliContext) { const options = args as { + files?: string[]; source: string; format: string; transport?: string; @@ -159,6 +180,7 @@ export const exportCommand: CliCommandPlugin = { types?: string; dryRun?: boolean; activate?: boolean; + unlock?: boolean; abapLanguageVersion?: string; }; @@ -176,16 +198,42 @@ export const exportCommand: CliCommandPlugin = { process.exit(1); } + // When specific files are provided, auto-resolve the abapGit repo root + const specificFiles = + options.files && options.files.length > 0 ? options.files : undefined; + let sourcePath = options.source; + + if (specificFiles) { + const repoRoot = findAbapGitRoot(ctx.cwd); + if (!repoRoot) { + ctx.logger.error('❌ No .abapgit.xml found in any parent directory.'); + ctx.logger.error( + ' Run this command from within an abapGit repository.', + ); + process.exit(1); + } + sourcePath = repoRoot; + } + ctx.logger.info('🚀 Starting export...'); - ctx.logger.info(`📁 Source: ${options.source}`); + ctx.logger.info(`📁 Source: ${sourcePath}`); ctx.logger.info(`🎯 Format: ${options.format}`); + if (specificFiles) ctx.logger.info(`📄 Files: ${specificFiles.join(', ')}`); if (options.transport) ctx.logger.info(`🚚 Transport: ${options.transport}`); if (options.package) ctx.logger.info(`📦 Package: ${options.package}`); if (options.dryRun) ctx.logger.info(`🔍 Mode: Dry run (no changes)`); - // Create FileTree from source path - const fileTree = createFileTree(options.source); + // Create FileTree — wrap with filter when deploying specific files + let fileTree = createFileTree(sourcePath); + if (specificFiles) { + const relFiles = resolveFilesRelativeToRoot( + specificFiles, + ctx.cwd, + sourcePath, + ); + fileTree = new FilteredFileTree(fileTree, relFiles); + } // Parse object types filter const objectTypes = options.types @@ -322,6 +370,18 @@ export const exportCommand: CliCommandPlugin = { if (!exists) { ctx.logger.info(` 📦 Creating subpackage ${pkgName}...`); try { + // Build transport config — SAP requires both softwareComponent + // and transportLayer when transport element is present + const swComp = parentPkg?.transport?.softwareComponent?.name; + const trLayer = parentPkg?.transport?.transportLayer?.name; + const transportConfig = + swComp || trLayer + ? { + softwareComponent: { name: swComp ?? '' }, + transportLayer: { name: trLayer ?? '' }, + } + : undefined; + await AdkPackage.create( pkgName, { @@ -331,17 +391,15 @@ export const exportCommand: CliCommandPlugin = { attributes: { packageType: 'development', ...(options.abapLanguageVersion - ? { languageVersion: options.abapLanguageVersion } + ? { + languageVersion: options.abapLanguageVersion as + | '' + | '2' + | '5', + } : {}), }, - transport: { - softwareComponent: parentPkg?.transport?.softwareComponent - ? { name: parentPkg.transport.softwareComponent.name } - : undefined, - transportLayer: parentPkg?.transport?.transportLayer - ? { name: parentPkg.transport.transportLayer.name } - : undefined, - }, + ...(transportConfig ? { transport: transportConfig } : {}), }, { transport: options.transport }, adkContext, @@ -372,9 +430,17 @@ export const exportCommand: CliCommandPlugin = { if (!targetPkg) continue; try { + // Save local _data before load() — load() overwrites _data with + // SAP's current server state, destroying abapGit-sourced fields. + const localData = (obj as any)._data; + // Try loading existing object from SAP await obj.load(); const currentPkg = (obj as any).package as string | undefined; + + // Restore the abapGit-sourced data (load was only for package check) + (obj as any)._data = localData; + if (!currentPkg || currentPkg === targetPkg) continue; // Object exists in wrong package — delete so deploy recreates it @@ -410,17 +476,41 @@ export const exportCommand: CliCommandPlugin = { } } + // ============================================ + // Pre-deploy: force-unlock all objects (--unlock) + // ============================================ + if (options.unlock && !options.dryRun) { + ctx.logger.info( + `\n� --unlock: Force-unlocking all ${objectSet.size} objects...`, + ); + let unlocked = 0; + for (const obj of objectSet) { + try { + await (client as any).fetch(`${obj.objectUri}?_action=UNLOCK`, { + method: 'POST', + headers: { 'X-sap-adt-sessiontype': 'stateful' }, + }); + unlocked++; + } catch { + // Object wasn't locked — that's fine, ignore + } + } + if (unlocked > 0) { + ctx.logger.info(` 🔓 ${unlocked} object(s) unlocked`); + } + } + // ============================================ // Deploy using AdkObjectSet (save + activate) // ============================================ if (!options.dryRun) { - ctx.logger.info(`\n🚀 Deploying ${objectSet.size} objects...`); + ctx.logger.info(`\n� Deploying ${objectSet.size} objects...`); // Use AdkObjectSet.deploy() for save inactive + bulk activate // Use 'upsert' mode: tries lock first (update existing), falls back to create if not found const deployResult = await objectSet.deploy({ transport: options.transport, - activate: options.activate !== false, + activate: options.activate, mode: 'upsert', onProgress: (saved, total, obj) => { if (obj._unchanged) { diff --git a/packages/adt-export/src/commands/roundtrip.ts b/packages/adt-export/src/commands/roundtrip.ts new file mode 100644 index 00000000..8bed1b76 --- /dev/null +++ b/packages/adt-export/src/commands/roundtrip.ts @@ -0,0 +1,565 @@ +/** + * Roundtrip Command Plugin + * + * Deploys local files to SAP, imports them back, and compares + * the original with the re-serialized result. + * + * Usage: + * adt roundtrip zage_fixed_values.doma.xml -p ZABAPGIT_EXAMPLES + * adt roundtrip *.doma.xml -p ZABAPGIT_EXAMPLES (glob pattern) + * adt roundtrip -s ./my-repo -p ZPACKAGE (all files) + */ + +import type { + CliCommandPlugin, + CliContext, + AdtPlugin, + ExportOptions, + ImportContext, +} from '@abapify/adt-plugin'; +import { + AdkObjectSet, + AdkPackage, + type AdkContext, + type AdkObject, + type AdtClient, + createAdk, +} from '@abapify/adk'; +import { + createFileTree, + FilteredFileTree, + findAbapGitRoot, + resolveFilesRelativeToRoot, +} from '../utils/filetree'; +import { + mkdirSync, + rmSync, + readFileSync, + writeFileSync, + readdirSync, + statSync, +} from 'node:fs'; +import { glob as nativeGlob } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import { tmpdir } from 'node:os'; +import { createTwoFilesPatch } from 'diff'; +import chalk from 'chalk'; +import { XMLParser, XMLBuilder } from 'fast-xml-parser'; + +/** + * Format shortcuts + */ +const FORMAT_SHORTCUTS: Record = { + abapgit: '@abapify/adt-plugin-abapgit', + ag: '@abapify/adt-plugin-abapgit', +}; + +async function loadFormatPlugin(formatSpec: string): Promise { + const packageName = FORMAT_SHORTCUTS[formatSpec] ?? formatSpec; + const pluginModule = await import(packageName); + const PluginClass = + pluginModule.default || pluginModule[Object.keys(pluginModule)[0]]; + if (!PluginClass) { + throw new Error(`No plugin class found in ${packageName}`); + } + return typeof PluginClass === 'function' && PluginClass.prototype + ? new PluginClass() + : PluginClass; +} + +/** + * Resolve full package path from SAP (walks super packages upward) + * Uses global ADK context (set when client is initialized) + */ +async function resolvePackagePath(packageName: string): Promise { + const path: string[] = []; + let current = packageName; + while (current) { + path.unshift(current); + try { + const pkg = await AdkPackage.get(current); + const superPkg = pkg.superPackage; + if (superPkg?.name) { + current = superPkg.name; + } else { + break; + } + } catch { + break; + } + } + return path; +} + +/** + * Normalize XML for comparison + * - Preserve namespace prefixes and declarations + * - Normalize whitespace and indentation + * - Format attributes on separate lines + */ +function normalizeXml(xml: string): string { + try { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + trimValues: true, + parseTagValue: false, + parseAttributeValue: false, + removeNSPrefix: false, // Keep namespace prefixes + }); + + const builder = new XMLBuilder({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + format: true, + indentBy: ' ', + suppressEmptyNode: false, + }); + + // Parse → normalize → rebuild + const obj = parser.parse(xml); + let normalized = builder.build(obj); + + // Format attributes on separate lines for better diff readability + normalized = normalized.replace( + /<([^\s>]+)((?:\s+[^\s=]+="[^"]*")+)\s*(\/?)>/g, + (match, tag, attrs, selfClose) => { + const attrList = attrs + .trim() + .split(/\s+(?=[^\s=]+=)/) + .map((a: string) => `\n ${a}`) + .join(''); + return `<${tag}${attrList}\n${selfClose ? '/' : ''}>`; + }, + ); + + return normalized; + } catch (err) { + // If parsing fails, return trimmed original + return xml.trim(); + } +} + +/** + * Recursively collect all files in a directory (relative paths) + */ +function collectFiles(dir: string, base?: string): string[] { + const result: string[] = []; + const baseDir = base ?? dir; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + const rel = relative(baseDir, full); + if (statSync(full).isDirectory()) { + result.push(...collectFiles(full, baseDir)); + } else { + result.push(rel); + } + } + return result; +} + +/** + * Expand glob patterns in a list of file arguments. + * Literal filenames (no glob characters) pass through unchanged. + */ +async function expandGlobs(patterns: string[], cwd: string): Promise { + const results: string[] = []; + for (const pattern of patterns) { + if (/[*?[\]{}]/.test(pattern)) { + for await (const match of nativeGlob(pattern, { cwd })) { + results.push(match); + } + } else { + results.push(pattern); + } + } + return results; +} + +export const roundtripCommand: CliCommandPlugin = { + name: 'roundtrip', + description: 'Deploy files to SAP, import back, and compare (roundtrip test)', + + arguments: [ + { + name: '[files...]', + description: + 'Specific files or glob patterns to test (e.g., zage_fixed_values.doma.xml, *.doma.xml). Omit to test all.', + }, + ], + + options: [ + { + flags: '-s, --source ', + description: 'Source directory containing serialized files', + default: '.', + }, + { + flags: '-f, --format ', + description: 'Format plugin: abapgit | @abapify/adt-plugin-abapgit', + default: 'abapgit', + }, + { + flags: '-p, --package ', + description: 'Target package for new objects', + }, + { + flags: '-t, --transport ', + description: 'Transport request for changes', + }, + { + flags: '--types ', + description: 'Filter by object types (comma-separated)', + }, + { + flags: '--activate', + description: 'Activate objects after deploy (use --no-activate to skip)', + default: true, + }, + { + flags: '--keep-tmp', + description: 'Keep temporary reimport directory for inspection', + default: false, + }, + { + flags: '--abap-language-version ', + description: 'ABAP language version for new objects (2=keyUser, 5=cloud)', + }, + ], + + async execute(args: Record, ctx: CliContext) { + const options = args as { + files?: string[]; + source: string; + format: string; + transport?: string; + package?: string; + types?: string; + activate?: boolean; + keepTmp?: boolean; + abapLanguageVersion?: string; + }; + + if (!ctx.getAdtClient) { + ctx.logger.error('❌ ADT client not available. Run: adt auth login'); + process.exit(1); + } + + // ── Resolve source path ── + const expandedFiles = + options.files && options.files.length > 0 + ? await expandGlobs(options.files, ctx.cwd) + : undefined; + const specificFiles = + expandedFiles && expandedFiles.length > 0 ? expandedFiles : undefined; + let sourcePath = options.source; + + if (specificFiles) { + const repoRoot = findAbapGitRoot(ctx.cwd); + if (!repoRoot) { + ctx.logger.error('❌ No .abapgit.xml found in any parent directory.'); + process.exit(1); + } + sourcePath = repoRoot; + } + + ctx.logger.info('🔄 Roundtrip test'); + ctx.logger.info(`📁 Source: ${sourcePath}`); + if (specificFiles) ctx.logger.info(`📄 Files: ${specificFiles.join(', ')}`); + if (options.package) ctx.logger.info(`📦 Package: ${options.package}`); + + // ── Build FileTree ── + let fileTree = createFileTree(sourcePath); + if (specificFiles) { + const relFiles = resolveFilesRelativeToRoot( + specificFiles, + ctx.cwd, + sourcePath, + ); + fileTree = new FilteredFileTree(fileTree, relFiles); + } + + const client = await ctx.getAdtClient!(); + const adkContext: AdkContext = { client: client as any }; + + // ── Phase 1: Deploy ── + ctx.logger.info('\n═══ Phase 1: Deploy to SAP ═══'); + + const plugin = await loadFormatPlugin(options.format); + if (!plugin.format.export) { + ctx.logger.error(`❌ Plugin '${plugin.name}' does not support export`); + process.exit(1); + } + + const exportOptions: ExportOptions = { + rootPackage: options.package, + abapLanguageVersion: options.abapLanguageVersion, + }; + + // Parse object types filter + const objectTypes = options.types + ? options.types.split(',').map((t: string) => t.trim().toUpperCase()) + : undefined; + + const deployedObjects: AdkObject[] = []; + const objectSet = await AdkObjectSet.fromGenerator( + plugin.format.export(fileTree, client, exportOptions), + adkContext, + { + filter: objectTypes + ? (obj) => { + const objType = obj.type.toUpperCase(); + const objPrefix = objType.split('/')[0]; + return ( + objectTypes.includes(objType) || objectTypes.includes(objPrefix) + ); + } + : undefined, + onObject: (obj) => { + ctx.logger.info(` 📄 ${obj.kind} ${obj.name}`); + }, + }, + ); + + const deployResult = await objectSet.deploy({ + transport: options.transport, + activate: options.activate, + mode: 'upsert', + onProgress: (saved, total, obj) => { + if (obj._unchanged) { + ctx.logger.info( + ` ⏭️ [${saved}/${total}] ${obj.kind} ${obj.name} (unchanged)`, + ); + } else { + ctx.logger.info(` 💾 [${saved}/${total}] ${obj.kind} ${obj.name}`); + } + }, + }); + + ctx.logger.info( + ` ✅ Deployed: ${deployResult.save.success} saved, ${deployResult.save.unchanged} unchanged, ${deployResult.save.failed} failed`, + ); + + if (deployResult.save.failed > 0) { + ctx.logger.error('❌ Deploy had failures — aborting roundtrip.'); + process.exit(1); + } + + // Collect deployed objects for reimport + for (const obj of objectSet) { + deployedObjects.push(obj); + } + + // ── Phase 2: Import back from SAP ── + ctx.logger.info('\n═══ Phase 2: Import back from SAP ═══'); + + if (!plugin.format.import) { + ctx.logger.error(`❌ Plugin '${plugin.name}' does not support import`); + process.exit(1); + } + + const tmpDir = join(tmpdir(), `adt-roundtrip-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + ctx.logger.info(` 📂 Temp dir: ${tmpDir}`); + + const adk = createAdk(client as AdtClient); + let reimported = 0; + let reimportFailed = 0; + + for (const obj of deployedObjects) { + try { + // Load fresh from SAP + const fresh = adk.get(obj.name, obj.type); + await (fresh as any).load(); + + // Build import context with package path resolver + const importContext: ImportContext = { + resolvePackagePath, + }; + + // Serialize back to local files using format plugin + const result = await plugin.format.import( + fresh as any, + tmpDir, + importContext, + ); + + if (result.success) { + reimported++; + ctx.logger.info(` ✅ ${obj.kind} ${obj.name}`); + } else { + reimportFailed++; + ctx.logger.error( + ` ❌ ${obj.kind} ${obj.name}: ${result.errors?.join(', ') || 'unknown'}`, + ); + } + } catch (err) { + reimportFailed++; + ctx.logger.error( + ` ❌ ${obj.kind} ${obj.name}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + + // Call afterImport hook (generates .abapgit.xml etc) + if (plugin.hooks?.afterImport) { + await plugin.hooks.afterImport(tmpDir); + } + + ctx.logger.info( + ` 📊 Reimported: ${reimported} success, ${reimportFailed} failed`, + ); + + // ── Phase 3: Compare ── + ctx.logger.info('\n═══ Phase 3: Compare ═══'); + + // Collect re-imported files (skip .abapgit.xml — generated metadata) + const reimportedFiles = collectFiles(tmpDir).filter( + (f) => !f.endsWith('.abapgit.xml'), + ); + + let matches = 0; + let mismatches = 0; + let onlyOriginal = 0; + let onlyReimported = 0; + + // Map reimported files by their filename (last segment) for matching + const reimportedByName = new Map(); + for (const f of reimportedFiles) { + const name = f.split('/').pop()!; + reimportedByName.set(name, f); + } + + // Get original files from the FileTree + const originalXml = await fileTree.glob('**/*.xml'); + const originalAbap = await fileTree.glob('**/*.abap'); + const allOriginal = [...originalXml, ...originalAbap].filter( + (f) => !f.endsWith('.abapgit.xml') && !f.endsWith('package.devc.xml'), + ); + + for (const origRelPath of allOriginal) { + const origName = origRelPath.split('/').pop()!; + const reimportedRelPath = reimportedByName.get(origName); + + if (!reimportedRelPath) { + onlyOriginal++; + ctx.logger.warn(` ⚠️ Only in original: ${origName}`); + continue; + } + + // Remove from map so we can track reimported-only files + reimportedByName.delete(origName); + + // Read original from FileTree, reimported from disk + let origContent = (await fileTree.read(origRelPath)).trim(); + let reimContent = readFileSync( + join(tmpDir, reimportedRelPath), + 'utf-8', + ).trim(); + + // Normalize XML files before comparison + const isXml = origName.endsWith('.xml'); + if (isXml) { + origContent = normalizeXml(origContent); + reimContent = normalizeXml(reimContent); + + // Save normalized files for inspection + if (options.keepTmp) { + writeFileSync( + join(tmpDir, 'normalized-original-' + origName), + origContent, + 'utf-8', + ); + writeFileSync( + join(tmpDir, 'normalized-reimported-' + origName), + reimContent, + 'utf-8', + ); + } + } + + // Compare content + if (origContent === reimContent) { + matches++; + ctx.logger.info(` ✅ ${origName}`); + } else { + mismatches++; + ctx.logger.error(` ❌ ${origName} — DIFFERS`); + + // Generate unified diff + const patch = createTwoFilesPatch( + `original/${origName}`, + `reimported/${origName}`, + origContent, + reimContent, + '', + '', + { context: 3 }, + ); + + // Show diff with colors (skip file headers, keep hunk headers) + const diffLines = patch.split('\n'); + let lineCount = 0; + const maxLines = 50; + + for (const line of diffLines) { + // Skip file path headers (--- and +++) + if (line.startsWith('---') || line.startsWith('+++')) { + continue; + } + + if (lineCount >= maxLines) { + console.log( + chalk.yellow( + ' ... (truncated, use --keep-tmp to inspect full files)', + ), + ); + break; + } + + if (line.startsWith('+')) { + console.log(chalk.green(` ${line}`)); + } else if (line.startsWith('-')) { + console.log(chalk.red(` ${line}`)); + } else if (line.startsWith('@@')) { + console.log(chalk.cyan(` ${line}`)); + } else if (line.trim()) { + console.log(chalk.gray(` ${line}`)); + } + + lineCount++; + } + } + } + + // Files only in reimported + for (const [name] of reimportedByName) { + onlyReimported++; + ctx.logger.warn(` ⚠️ Only in reimported: ${name}`); + } + + // ── Summary ── + ctx.logger.info('\n═══ Summary ═══'); + ctx.logger.info(` ✅ Matching: ${matches}`); + if (mismatches > 0) ctx.logger.error(` ❌ Mismatches: ${mismatches}`); + if (onlyOriginal > 0) + ctx.logger.warn(` ⚠️ Only in original: ${onlyOriginal}`); + if (onlyReimported > 0) + ctx.logger.warn(` ⚠️ Only in reimported: ${onlyReimported}`); + + if (options.keepTmp) { + ctx.logger.info(`\n📂 Temp dir kept: ${tmpDir}`); + } else { + rmSync(tmpDir, { recursive: true, force: true }); + } + + if (mismatches > 0) { + ctx.logger.error('\n❌ Roundtrip test FAILED'); + process.exit(1); + } + + ctx.logger.info('\n✅ Roundtrip test PASSED'); + }, +}; + +export default roundtripCommand; diff --git a/packages/adt-export/src/index.ts b/packages/adt-export/src/index.ts index 3bb07f84..a0251bb7 100644 --- a/packages/adt-export/src/index.ts +++ b/packages/adt-export/src/index.ts @@ -15,7 +15,14 @@ */ export { exportCommand } from './commands/export'; -export { createFileTree, FsFileTree, MemoryFileTree } from './utils/filetree'; +export { + createFileTree, + FsFileTree, + MemoryFileTree, + FilteredFileTree, + findAbapGitRoot, + resolveFilesRelativeToRoot, +} from './utils/filetree'; export type { FileTree, ExportResult, diff --git a/packages/adt-export/src/utils/filetree.ts b/packages/adt-export/src/utils/filetree.ts index 2c83941f..bec1b3bc 100644 --- a/packages/adt-export/src/utils/filetree.ts +++ b/packages/adt-export/src/utils/filetree.ts @@ -8,7 +8,8 @@ import { access, glob as nativeGlob, } from 'node:fs/promises'; -import { join } from 'node:path'; +import { join, relative, resolve, dirname } from 'node:path'; +import { existsSync } from 'node:fs'; import type { FileTree } from '@abapify/adt-plugin'; /** @@ -118,3 +119,94 @@ export class MemoryFileTree implements FileTree { export function createFileTree(sourcePath: string): FileTree { return new FsFileTree(sourcePath); } + +/** + * FileTree wrapper that filters glob results to only include specified files. + * Metadata files (.abapgit.xml) always pass through. + */ +export class FilteredFileTree implements FileTree { + private readonly allowedFiles: Set; + + constructor( + private readonly inner: FileTree, + files: string[], + ) { + this.allowedFiles = new Set(files); + } + + get root(): string { + return this.inner.root; + } + + async glob(pattern: string): Promise { + const all = await this.inner.glob(pattern); + return all.filter((f) => this.isAllowed(f)); + } + + read(path: string): Promise { + return this.inner.read(path); + } + + readBuffer(path: string): Promise { + return this.inner.readBuffer(path); + } + + exists(path: string): Promise { + return this.inner.exists(path); + } + + readdir(path: string): Promise { + return this.inner.readdir(path); + } + + private isAllowed(filePath: string): boolean { + // Always allow metadata files + if (filePath.endsWith('.abapgit.xml')) return true; + + // Check if any allowed file pattern matches + const filename = filePath.split('/').pop()!; + // Match the base name (before type extension) — e.g. "zage_fixed_values" + // from "zage_fixed_values.doma.xml" to also include companion .abap files + for (const allowed of this.allowedFiles) { + if (filePath === allowed || filename === allowed) return true; + // Match companion files: same base name (e.g., myobj.clas.xml matches myobj.clas.abap) + const allowedBase = allowed.replace(/\.\w+$/, ''); // strip last extension + const fileBase = filename.replace(/\.\w+$/, ''); // strip last extension + if (fileBase === allowedBase) return true; + } + return false; + } +} + +/** + * Walk up from a directory to find the nearest ancestor containing .abapgit.xml. + * Returns the resolved absolute path to the repo root, or undefined if not found. + */ +export function findAbapGitRoot(startDir: string): string | undefined { + let dir = resolve(startDir); + const root = resolve('/'); + + while (dir !== root) { + if (existsSync(join(dir, '.abapgit.xml'))) { + return dir; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return undefined; +} + +/** + * Convert absolute file paths to paths relative to the repo root. + */ +export function resolveFilesRelativeToRoot( + files: string[], + cwd: string, + repoRoot: string, +): string[] { + return files.map((f) => { + const abs = resolve(cwd, f); + return relative(repoRoot, abs); + }); +} diff --git a/packages/adt-export/tsdown.config.ts b/packages/adt-export/tsdown.config.ts index 72d7dad3..d0858732 100644 --- a/packages/adt-export/tsdown.config.ts +++ b/packages/adt-export/tsdown.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ - entry: ['src/index.ts', 'src/commands/export.ts'], + entry: [ + 'src/index.ts', + 'src/commands/export.ts', + 'src/commands/roundtrip.ts', + 'src/commands/activate.ts', + ], format: ['esm'], dts: true, clean: true, diff --git a/packages/adt-mcp/tsconfig.json b/packages/adt-mcp/tsconfig.json index b1b4f37d..e8040183 100644 --- a/packages/adt-mcp/tsconfig.json +++ b/packages/adt-mcp/tsconfig.json @@ -16,5 +16,13 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [ + { + "path": "../adt-contracts" + }, + { + "path": "../adt-client" + } + ] } diff --git a/packages/adt-plugin-abapgit/package.json b/packages/adt-plugin-abapgit/package.json index 878f8f48..e0f19dca 100644 --- a/packages/adt-plugin-abapgit/package.json +++ b/packages/adt-plugin-abapgit/package.json @@ -18,6 +18,7 @@ "xsd" ], "dependencies": { + "@abapify/acds": "workspace:*", "@abapify/adk": "^0.1.7", "@abapify/adt-atc": "^0.1.7", "@abapify/adt-plugin": "^0.1.7", diff --git a/packages/adt-plugin-abapgit/project.json b/packages/adt-plugin-abapgit/project.json index de915f06..76e6e0a8 100644 --- a/packages/adt-plugin-abapgit/project.json +++ b/packages/adt-plugin-abapgit/project.json @@ -2,7 +2,7 @@ "name": "adt-plugin-abapgit", "targets": { "codegen": { - "command": "npx ts-xsd codegen --verbose", + "command": "bunx ts-xsd codegen --verbose", "options": { "cwd": "{projectRoot}" }, diff --git a/packages/adt-plugin-abapgit/src/lib/deserializer.ts b/packages/adt-plugin-abapgit/src/lib/deserializer.ts index 2d41532e..39e1912c 100644 --- a/packages/adt-plugin-abapgit/src/lib/deserializer.ts +++ b/packages/adt-plugin-abapgit/src/lib/deserializer.ts @@ -81,18 +81,19 @@ export async function* deserialize( // Get ADK factory for creating objects const adk = createAdk(client); - // Resolve folder logic from .abapgit.xml (if present) + // Resolve folder logic from .abapgit.xml (optional — defaults used when missing) let folderLogic: import('./folder-logic').FolderLogic = 'prefix'; - let startDir = 'src'; - try { - if (await fileTree.exists('.abapgit.xml')) { + let startDir = ''; + const hasAbapGitXml = await fileTree.exists('.abapgit.xml'); + if (hasAbapGitXml) { + try { const xml = await fileTree.read('.abapgit.xml'); const meta = parseAbapGitMetadata(xml); folderLogic = meta.folderLogic; startDir = stripSlashes(meta.startingFolder); + } catch { + // Fall through to defaults if XML parsing fails } - } catch { - // Fall through to defaults } // Find all XML files (these define the objects) @@ -160,7 +161,7 @@ export async function* deserialize( const xmlContent = await fileTree.read(objFiles.xmlFile); const parsed = handler.schema.parse(xmlContent); // Schema parses to { abapGit: { abap: { values: ... } } } - const values = (parsed as any)?.abapGit?.abap?.values; + const values = (parsed as any)?.abapGit?.abap?.values ?? {}; // Read source files, mapping suffixes using handler's suffixToSourceKey const sources: Record = {}; @@ -189,7 +190,11 @@ export async function* deserialize( const fullData = { ...payload, name: objectName }; // Create ADK object with data (pre-loaded, no need to call load()) - const adkObject = adk.getWithData(fullData, objFiles.type) as AdkObject; + // Use payload type if available (e.g., TABL/DS for structures), + // falling back to filename-derived type + const adkType = + typeof payload.type === 'string' ? payload.type : objFiles.type; + const adkObject = adk.getWithData(fullData, adkType) as AdkObject; // Set sources on object using handler's setSources method if (Object.keys(sources).length > 0) { @@ -211,22 +216,31 @@ export async function* deserialize( (adkObject as any)._pendingDescription = payload.description; } - // Resolve packageRef using abapGit folder logic + // Resolve packageRef if (options?.rootPackage) { const data = (adkObject as any)._data; if (data && !data.packageRef) { - const sourceDir = objFiles.xmlFile.split('/').slice(0, -1).join('/'); - // Strip starting folder prefix to get relative path - const relDir = sourceDir.startsWith(startDir) - ? sourceDir.slice(startDir.length).replace(/^\/+/, '') - : sourceDir; - - const pkgName = resolvePackageFromDir( - relDir, - folderLogic, - options.rootPackage, - ); - data.packageRef = { name: pkgName }; + if (hasAbapGitXml) { + // Folder logic: resolve subpackage from directory structure + const sourceDir = objFiles.xmlFile + .split('/') + .slice(0, -1) + .join('/'); + // Strip starting folder prefix to get relative path + const relDir = sourceDir.startsWith(startDir) + ? sourceDir.slice(startDir.length).replace(/^\/+/, '') + : sourceDir; + + const pkgName = resolvePackageFromDir( + relDir, + folderLogic, + options.rootPackage, + ); + data.packageRef = { name: pkgName }; + } else { + // No .abapgit.xml: assign rootPackage directly + data.packageRef = { name: options.rootPackage }; + } } } diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/base.ts b/packages/adt-plugin-abapgit/src/lib/handlers/base.ts index 1d5d124d..690d1c75 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/base.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/base.ts @@ -6,7 +6,7 @@ */ import type { AdkObject, AdkKind } from '@abapify/adk'; -import { getTypeForKind } from '@abapify/adk'; +import { getTypeForKind, getMainType } from '@abapify/adk'; import type { AbapGitSchema, InferAbapGitType, @@ -374,7 +374,7 @@ export function createHandler< type = derivedType; } - const fileExtension = type.toLowerCase(); + const fileExtension = getMainType(type).toLowerCase(); // Create handler context with utilities const ctx: HandlerContext> = { @@ -403,6 +403,7 @@ export function createHandler< // Construct full AbapGitType payload const fullPayload = { abap: { + version: '1.0', // Explicit version attribute for asx:abap values, }, version: definition.version, @@ -411,7 +412,28 @@ export function createHandler< } as InferAbapGitType; // Build XML with pretty formatting for readability - return definition.schema.build(fullPayload, { pretty: true }); + let xml = definition.schema.build(fullPayload, { pretty: true }); + + // Format attributes on separate lines for better diff readability + xml = xml.replace( + /<([^\s>]+)((?:\s+[^\s=]+="[^"]*")+)\s*(\/?)>/g, + (match, tag, attrs, selfClose) => { + const attrList = attrs + .trim() + .split(/\s+(?=[^\s=]+=)/) + .map((a: string) => `\n ${a}`) + .join(''); + return `<${tag}${attrList}\n${selfClose ? '/' : ''}>`; + }, + ); + + // Move xmlns:asx from root to asx:abap element (abapGit format convention) + xml = xml.replace( + /(]*)\s+xmlns:asx="http:\/\/www\.sap\.com\/abapxml"([^>]*>[\s\S]*?)( a.key === key); + return ann ? annotationStringValue(ann.value) : undefined; +} + +// ============================================ +// Annotation → DD02V Mapping +// ============================================ + +const TABLE_CATEGORY_MAP: Record = { + TRANSPARENT: 'TRANSP', + CLUSTER: 'CLUSTER', + POOL: 'POOL', + APPEND: 'APPEND', +}; + +const ENHANCEMENT_CATEGORY_MAP: Record = { + NOT_EXTENSIBLE: '1', + EXTENSIBLE_CHARACTER_NUMERIC: '2', + EXTENSIBLE_CHARACTER: '3', + EXTENSIBLE_ANY: '4', + NOT_CLASSIFIED: '0', +}; + +const DATA_MAINTENANCE_MAP: Record = { + RESTRICTED: 'X', + NOT_ALLOWED: 'N', + ALLOWED: '', +}; + +/** + * Build DD02V data from a parsed CDS table/structure definition + */ +export function buildDD02V( + def: TableDefinition | StructureDefinition, + language: string, + description: string, +): DD02VData { + // Compute all values first, then assemble in abapGit field order + let tabclass = def.kind === 'structure' ? 'INTTAB' : 'TRANSP'; + + const tableCategory = getAnnotation( + def.annotations, + 'AbapCatalog.tableCategory', + ); + if (tableCategory) { + tabclass = TABLE_CATEGORY_MAP[tableCategory] || tabclass; + } + + const deliveryClass = getAnnotation( + def.annotations, + 'AbapCatalog.deliveryClass', + ); + + const enhancementCategory = getAnnotation( + def.annotations, + 'AbapCatalog.enhancement.category', + ); + + // CLIDEP — set to 'X' if table has a client-type field + const fields = def.members.filter( + (m): m is FieldDefinition => !('kind' in m && m.kind === 'include'), + ); + const hasClientField = fields.some( + (f) => + (f.type.kind === 'builtin' && f.type.name === 'clnt') || + (f.type.kind === 'named' && f.type.name.toLowerCase() === 'mandt'), + ); + + // Build result in standard abapGit DD02V field order: + // TABNAME, DDLANGUAGE, TABCLASS, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS + const result: DD02VData = {}; + result.TABNAME = def.name.toUpperCase(); + result.DDLANGUAGE = language; + result.TABCLASS = tabclass; + if (hasClientField) result.CLIDEP = 'X'; + result.DDTEXT = description; + result.MASTERLANG = language; + if (deliveryClass) result.CONTFLAG = deliveryClass; + if (enhancementCategory) + result.EXCLASS = ENHANCEMENT_CATEGORY_MAP[enhancementCategory]; + + return result; +} + +// ============================================ +// Field → DD03P Mapping +// ============================================ + +interface BuiltinType { + datatype: string; + inttype: string; + length?: number; + decimals?: number; + fixedIntlen?: number; +} + +const BUILTIN_TYPES: Record = { + char: { datatype: 'CHAR', inttype: 'C' }, + clnt: { datatype: 'CLNT', inttype: 'C', length: 3 }, + numc: { datatype: 'NUMC', inttype: 'N' }, + dats: { datatype: 'DATS', inttype: 'D', length: 8 }, + tims: { datatype: 'TIMS', inttype: 'T', length: 6 }, + string: { datatype: 'STRG', inttype: 'g', fixedIntlen: 8 }, + xstring: { datatype: 'RSTR', inttype: 'h', fixedIntlen: 8 }, + int1: { datatype: 'INT1', inttype: 'b', fixedIntlen: 3 }, + int2: { datatype: 'INT2', inttype: 's', fixedIntlen: 5 }, + int4: { datatype: 'INT4', inttype: 'I', fixedIntlen: 4 }, + int8: { datatype: 'INT8', inttype: '8', fixedIntlen: 8 }, + fltp: { datatype: 'FLTP', inttype: 'F', fixedIntlen: 8 }, + dec: { datatype: 'DEC', inttype: 'P' }, + curr: { datatype: 'CURR', inttype: 'P' }, + quan: { datatype: 'QUAN', inttype: 'P' }, + raw: { datatype: 'RAW', inttype: 'X' }, + rawstring: { datatype: 'RSTR', inttype: 'h', fixedIntlen: 8 }, + lang: { datatype: 'LANG', inttype: 'C', length: 1 }, + unit: { datatype: 'UNIT', inttype: 'C' }, + cuky: { datatype: 'CUKY', inttype: 'C' }, + d16n: { datatype: 'D16N', inttype: 'a', fixedIntlen: 8 }, + d34n: { datatype: 'D34N', inttype: 'e', fixedIntlen: 16 }, + utclong: { datatype: 'UTCL', inttype: 'p', fixedIntlen: 8 }, +}; + +/** + * Types that always get a MASK (2 spaces + DATATYPE) + */ +const MASK_TYPES = new Set([ + 'clnt', + 'string', + 'xstring', + 'rawstring', + 'lang', + 'unit', + 'cuky', + 'dats', + 'tims', + 'dec', + 'curr', + 'quan', + 'd16n', + 'd34n', + 'utclong', +]); + +/** Zero-pad number to given width */ +function zeroPad(n: number, width: number): string { + return String(n).padStart(width, '0'); +} + +/** Compute internal length from type info */ +function computeIntlen(builtin: BuiltinType, length?: number): number { + if (builtin.fixedIntlen !== undefined) return builtin.fixedIntlen; + + // Packed decimal types: ceil((length + 1) / 2) + if (builtin.inttype === 'P' && length !== undefined) { + return Math.ceil((length + 1) / 2); + } + + // Raw bytes: length directly + if (builtin.inttype === 'X' && length !== undefined) { + return length; + } + + // Character types: length * 2 (Unicode) + if (length !== undefined) { + return length * 2; + } + + // Fixed-length types with known length + if (builtin.length !== undefined) { + return builtin.length * 2; + } + + return 0; +} + +/** Build a DD03P entry from a single field definition */ +function buildFieldDD03P(field: FieldDefinition): DD03PData { + // Compute all values first + const isKey = field.isKey; + const notNull = field.notNull; + let rollname: string | undefined; + let comptype: string | undefined; + let inttype: string | undefined; + let intlen: string | undefined; + let datatype: string | undefined; + let leng: string | undefined; + let decimals: string | undefined; + let mask: string | undefined; + + if (field.type.kind === 'builtin') { + const bt = field.type as BuiltinTypeRef; + const typeInfo = BUILTIN_TYPES[bt.name]; + if (typeInfo) { + const length = bt.length ?? typeInfo.length; + inttype = typeInfo.inttype; + intlen = zeroPad(computeIntlen(typeInfo, length), 6); + datatype = typeInfo.datatype; + if (length !== undefined) leng = zeroPad(length, 6); + if (bt.decimals !== undefined) decimals = zeroPad(bt.decimals, 6); + if (MASK_TYPES.has(bt.name)) mask = ` ${typeInfo.datatype}`; + } + } else { + // Named type (data element reference) + rollname = field.type.name.toUpperCase(); + comptype = 'E'; + } + + // Build in standard abapGit DD03P field order: + // FIELDNAME, KEYFLAG, ROLLNAME, ADMINFIELD, INTTYPE, INTLEN, NOTNULL, DATATYPE, LENG, DECIMALS, MASK, COMPTYPE + const result: DD03PData = {}; + result.FIELDNAME = field.name.toUpperCase(); + if (isKey) result.KEYFLAG = 'X'; + if (rollname) result.ROLLNAME = rollname; + result.ADMINFIELD = '0'; + if (inttype) result.INTTYPE = inttype; + if (intlen) result.INTLEN = intlen; + if (notNull) result.NOTNULL = 'X'; + if (datatype) result.DATATYPE = datatype; + if (leng) result.LENG = leng; + if (decimals) result.DECIMALS = decimals; + if (mask) result.MASK = mask; + if (comptype) result.COMPTYPE = comptype; + + return result; +} + +/** + * Build DD03P entries from table/structure members + */ +export function buildDD03P(members: TableMember[]): DD03PData[] { + const fields = members.filter( + (m): m is FieldDefinition => !('kind' in m && m.kind === 'include'), + ); + return fields.map((field) => buildFieldDD03P(field)); +} diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/clas.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/clas.ts index b94ae40f..31f9aaa0 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/clas.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/clas.ts @@ -109,7 +109,7 @@ export const classHandler = createHandler(AdkClass, { }, // Git → SAP: Map abapGit values to ADK data (type inferred from AdkClass) - fromAbapGit: ({ VSEOCLASS }) => ({ + fromAbapGit: ({ VSEOCLASS } = {}) => ({ // Required - uppercase for SAP name: (VSEOCLASS?.CLSNAME ?? '').toUpperCase(), type: 'CLAS/OC', // ADT object type diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/devc.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/devc.ts index d322d607..b1284c97 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/devc.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/devc.ts @@ -31,7 +31,7 @@ export const packageHandler = createHandler(AdkPackage, { // Git → SAP: Map abapGit values to ADK data (type inferred from AdkPackage) // Note: DEVC doesn't have DEVCLASS in abapGit XML, name comes from filename - fromAbapGit: ({ DEVC }) => ({ + fromAbapGit: ({ DEVC } = {}) => ({ name: '', // Package name must be set by deserializer from filename description: DEVC?.CTEXT, }), diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/doma.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/doma.ts index 11aaa0a7..2d9cb9bf 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/doma.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/doma.ts @@ -17,28 +17,89 @@ export const domainHandler = createHandler(AdkDomain, { const data = obj.dataSync; const typeInfo = data?.content?.typeInformation; const outInfo = data?.content?.outputInformation; + const valueInfo = data?.content?.valueInformation; + + // Serialize fixed values if present + const fixValues = valueInfo?.fixValues?.fixValue; + const DD07V_TAB = + fixValues && fixValues.length > 0 + ? { + DD07V: fixValues.map((fv) => { + const entry: Record = { + DOMNAME: obj.name ?? '', + VALPOS: String(fv.position ?? '').padStart(4, '0'), + DDLANGUAGE: isoToSapLang(data?.language), + DOMVALUE_L: fv.low ?? '', + DDTEXT: fv.text ?? '', + }; + // Only include DOMVALUE_H if it has a value (for ranges) + if (fv.high) { + entry.DOMVALUE_H = fv.high; + } + return entry; + }), + } + : undefined; + return { DD01V: { DOMNAME: obj.name ?? '', DDLANGUAGE: isoToSapLang(data?.language), DATATYPE: typeInfo?.datatype ?? '', - LENG: String(typeInfo?.length ?? ''), - OUTPUTLEN: String(outInfo?.length ?? ''), - DECIMALS: String(typeInfo?.decimals ?? ''), - LOWERCASE: outInfo?.lowercase ? 'X' : '', - SIGNFLAG: outInfo?.signExists ? 'X' : '', - CONVEXIT: outInfo?.conversionExit ?? '', + LENG: String(typeInfo?.length ?? '').padStart(6, '0'), + DECIMALS: typeInfo?.decimals + ? String(typeInfo.decimals).padStart(6, '0') + : undefined, + OUTPUTLEN: String(outInfo?.length ?? '').padStart(6, '0'), + LOWERCASE: outInfo?.lowercase ? 'X' : undefined, + SIGNFLAG: outInfo?.signExists ? 'X' : undefined, + CONVEXIT: outInfo?.conversionExit || undefined, + ENTITYTAB: valueInfo?.valueTableRef?.name || undefined, + VALEXI: fixValues && fixValues.length > 0 ? 'X' : undefined, DDTEXT: obj.description ?? '', + DOMMASTER: isoToSapLang(obj.masterLanguage), }, + DD07V_TAB, }; }, - fromAbapGit: ({ DD01V }) => - ({ + fromAbapGit: ({ DD01V, DD07V_TAB } = {}) => { + const fixValues = + DD07V_TAB?.DD07V?.map((fv, idx) => ({ + position: Number(fv.VALPOS ?? idx + 1), + low: fv.DOMVALUE_L ?? '', + high: fv.DOMVALUE_H ?? '', + text: fv.DDTEXT ?? '', + })) ?? []; + + return { name: (DD01V?.DOMNAME ?? '').toUpperCase(), type: 'DOMA/DD', description: DD01V?.DDTEXT, language: sapLangToIso(DD01V?.DDLANGUAGE), masterLanguage: sapLangToIso(DD01V?.DDLANGUAGE), - }) as { name: string } & Record, + content: { + typeInformation: { + datatype: DD01V?.DATATYPE ?? '', + length: Number(DD01V?.LENG ?? 0), + decimals: Number(DD01V?.DECIMALS ?? 0), + }, + outputInformation: { + length: Number(DD01V?.OUTPUTLEN ?? 0), + style: '', + conversionExit: DD01V?.CONVEXIT || undefined, + signExists: DD01V?.SIGNFLAG === 'X', + lowercase: DD01V?.LOWERCASE === 'X', + }, + valueInformation: { + valueTableRef: { + name: DD01V?.ENTITYTAB || undefined, + }, + fixValues: { + fixValue: fixValues.length > 0 ? fixValues : undefined, + }, + }, + }, + } as { name: string } & Record; + }, }); diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/dtel.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/dtel.ts index e47d2267..58ea1d69 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/dtel.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/dtel.ts @@ -7,6 +7,104 @@ import { dtel } from '../../../schemas/generated'; import { createHandler } from '../base'; import { isoToSapLang, sapLangToIso } from '../lang'; +/** + * Map ADT typeKind → abapGit REFKIND + */ +const TYPEKIND_TO_REFKIND: Record = { + domain: 'D', + predefinedAbapType: '', + refToPredefinedAbapType: 'R', + refToDictionaryType: 'R', + refToClifType: 'R', +}; + +/** + * Map ADT typeKind → abapGit REFTYPE (only for reference types) + */ +const TYPEKIND_TO_REFTYPE: Record = { + refToClifType: 'C', + refToDictionaryType: 'E', + refToPredefinedAbapType: 'E', +}; + +/** + * Derive OUTPUTLEN from ABAP data type + length + decimals. + * + * The ADT REST API for data elements does not return OUTPUTLEN — SAP derives + * it internally from the type definition. This function replicates SAP's + * output length calculation for standard ABAP dictionary types. + */ +function deriveOutputLength( + dataType: string | undefined, + length: number | undefined, + decimals: number | undefined, +): string | undefined { + if (!dataType || !length) return undefined; + + const type = dataType.toUpperCase(); + const len = length; + const dec = decimals ?? 0; + + switch (type) { + // Simple types: output length = internal length + case 'CHAR': + case 'NUMC': + case 'UNIT': + case 'CUKY': + case 'LCHR': + case 'SSTRING': + return String(len).padStart(6, '0'); + + // Fixed-length types with known output length + case 'CLNT': + return '000003'; + case 'LANG': + return '000001'; + case 'DATS': + return '000010'; + case 'TIMS': + return '000008'; + + // Integer types + case 'INT1': + return '000003'; + case 'INT2': + return '000005'; + case 'INT4': + return '000010'; + case 'INT8': + return '000019'; + + // Floating point + case 'FLTP': + return '000022'; + + // Numeric types with formatting: len + sign + group separators + case 'DEC': + case 'CURR': + case 'QUAN': { + const intDigits = len - dec; + const separators = intDigits > 1 ? Math.floor((intDigits - 1) / 3) : 0; + const outputLen = len + 1 + separators; + return String(outputLen).padStart(6, '0'); + } + + // Raw/hex types: 2 hex chars per byte + case 'RAW': + case 'LRAW': + return String(len * 2).padStart(6, '0'); + + // Variable-length types have no meaningful output length + case 'STRING': + case 'RAWSTRING': + case 'GEOM_EWKB': + return undefined; + + default: + return undefined; + } +} + export const dataElementHandler = createHandler(AdkDataElement, { schema: dtel, version: 'v1.0.0', @@ -15,35 +113,118 @@ export const dataElementHandler = createHandler(AdkDataElement, { toAbapGit: (obj) => { const data = obj.dataSync; + const de = data?.dataElement; + const typeKind = de?.typeKind ?? ''; + + // Domain-based DTELs: DATATYPE/LENG/DECIMALS/OUTPUTLEN are inherited + // from the domain, not stored on the DTEL itself. abapGit omits them. + const isDomain = typeKind === 'domain'; + + // Reference types: abapGit emits DATATYPE=REF but ADT doesn't return it. + const isRef = typeKind.startsWith('refTo'); + const dataType = isRef ? 'REF' : de?.dataType || undefined; + + // Suppress domain-inherited type info + const effectiveDataType = isDomain ? undefined : dataType; + const effectiveLeng = + isDomain || !de?.dataTypeLength + ? undefined + : String(de.dataTypeLength).padStart(6, '0'); + const effectiveDecimals = + isDomain || !de?.dataTypeDecimals + ? undefined + : String(de.dataTypeDecimals).padStart(6, '0'); + const effectiveOutputLen = isDomain + ? undefined + : deriveOutputLength( + de?.dataType, + de?.dataTypeLength, + de?.dataTypeDecimals, + ); + + // Properties ordered to match canonical DD04V table column order. + // The XSD xs:sequence enforces this order in the XML output. return { DD04V: { ROLLNAME: obj.name ?? '', DDLANGUAGE: isoToSapLang(obj.language || undefined), + DOMNAME: de?.typeName || undefined, + HEADLEN: de?.headingFieldLength + ? String(de.headingFieldLength) + : undefined, + SCRLEN1: de?.shortFieldLength ? String(de.shortFieldLength) : undefined, + SCRLEN2: de?.mediumFieldLength + ? String(de.mediumFieldLength) + : undefined, + SCRLEN3: de?.longFieldLength ? String(de.longFieldLength) : undefined, DDTEXT: obj.description ?? '', - DOMNAME: data?.typeName ?? '', - DATATYPE: data?.dataType ?? '', - LENG: String(data?.dataTypeLength ?? ''), - DECIMALS: String(data?.dataTypeDecimals ?? ''), - REPTEXT: data?.headingFieldLabel ?? '', - SCRTEXT_S: data?.shortFieldLabel ?? '', - SCRTEXT_M: data?.mediumFieldLabel ?? '', - SCRTEXT_L: data?.longFieldLabel ?? '', - HEADLEN: String(data?.headingFieldLength ?? ''), - SCRLEN1: String(data?.shortFieldLength ?? ''), - SCRLEN2: String(data?.mediumFieldLength ?? ''), - SCRLEN3: String(data?.longFieldLength ?? ''), - REFKIND: data?.typeKind === 'domain' ? 'D' : '', + REPTEXT: de?.headingFieldLabel || undefined, + SCRTEXT_S: de?.shortFieldLabel || undefined, + SCRTEXT_M: de?.mediumFieldLabel || undefined, + SCRTEXT_L: de?.longFieldLabel || undefined, + DTELMASTER: isoToSapLang(obj.masterLanguage || undefined), + DATATYPE: effectiveDataType, + LENG: effectiveLeng, + DECIMALS: effectiveDecimals, + OUTPUTLEN: effectiveOutputLen, + REFKIND: TYPEKIND_TO_REFKIND[typeKind] || undefined, + REFTYPE: TYPEKIND_TO_REFTYPE[typeKind], }, }; }, - fromAbapGit: ({ DD04V }) => - ({ + fromAbapGit: ({ DD04V } = {}) => { + // Map REFKIND + REFTYPE to ADT typeKind enum + const refKind = DD04V?.REFKIND ?? ''; + const refType = DD04V?.REFTYPE ?? ''; + let typeKind: string; + if (refKind === 'D') { + typeKind = 'domain'; + } else if (refKind === 'R') { + typeKind = refType === 'C' ? 'refToClifType' : 'refToDictionaryType'; + } else { + typeKind = DD04V?.DATATYPE ? 'predefinedAbapType' : 'domain'; + } + + return { name: (DD04V?.ROLLNAME ?? '').toUpperCase(), type: 'DTEL/DE', description: DD04V?.DDTEXT, language: sapLangToIso(DD04V?.DDLANGUAGE), masterLanguage: sapLangToIso(DD04V?.DDLANGUAGE), abapLanguageVersion: DD04V?.ABAP_LANGUAGE_VERSION, - }) as { name: string } & Record, + dataElement: { + // SAP's strict xs:sequence parser requires ALL elements to be present. + // Every field in the dataelements.xsd sequence must have an explicit value. + typeKind, + typeName: DD04V?.DOMNAME || '', + dataType: DD04V?.DATATYPE || '', + dataTypeLength: DD04V?.LENG ? Number(DD04V.LENG) : 0, + dataTypeLengthEnabled: false, + dataTypeDecimals: DD04V?.DECIMALS ? Number(DD04V.DECIMALS) : 0, + dataTypeDecimalsEnabled: false, + shortFieldLabel: DD04V?.SCRTEXT_S || '', + shortFieldLength: DD04V?.SCRLEN1 ? Number(DD04V.SCRLEN1) : 0, + shortFieldMaxLength: DD04V?.SCRLEN1 ? Number(DD04V.SCRLEN1) : 0, + mediumFieldLabel: DD04V?.SCRTEXT_M || '', + mediumFieldLength: DD04V?.SCRLEN2 ? Number(DD04V.SCRLEN2) : 0, + mediumFieldMaxLength: DD04V?.SCRLEN2 ? Number(DD04V.SCRLEN2) : 0, + longFieldLabel: DD04V?.SCRTEXT_L || '', + longFieldLength: DD04V?.SCRLEN3 ? Number(DD04V.SCRLEN3) : 0, + longFieldMaxLength: DD04V?.SCRLEN3 ? Number(DD04V.SCRLEN3) : 0, + headingFieldLabel: DD04V?.REPTEXT || '', + headingFieldLength: DD04V?.HEADLEN ? Number(DD04V.HEADLEN) : 0, + headingFieldMaxLength: DD04V?.HEADLEN ? Number(DD04V.HEADLEN) : 0, + searchHelp: '', + searchHelpParameter: '', + setGetParameter: '', + defaultComponentName: '', + deactivateInputHistory: false, + changeDocument: false, + leftToRightDirection: false, + deactivateBIDIFiltering: false, + documentationStatus: '', + }, + } as { name: string } & Record; + }, }); diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/fugr.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/fugr.ts index c6566c69..02b923fe 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/fugr.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/fugr.ts @@ -22,7 +22,7 @@ export const functionGroupHandler = createHandler(AdkFunctionGroup, { // Git → SAP: Map abapGit values to ADK data // Note: FUGR doesn't have the group name in AREAT field; name comes from filename - fromAbapGit: ({ AREAT }) => ({ + fromAbapGit: ({ AREAT } = {}) => ({ name: '', // Function group name must be set by deserializer from filename type: 'FUGR/F', description: AREAT, diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/index.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/index.ts index 60969b1f..f03285c1 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/index.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/index.ts @@ -11,5 +11,5 @@ export { programHandler } from './prog'; export { functionGroupHandler } from './fugr'; export { domainHandler } from './doma'; export { dataElementHandler } from './dtel'; -export { tableHandler } from './tabl'; +export { tableHandler, structureHandler } from './tabl'; export { tableTypeHandler } from './ttyp'; diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/intf.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/intf.ts index 38c04c49..d7b5a078 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/intf.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/intf.ts @@ -27,7 +27,7 @@ export const interfaceHandler = createHandler(AdkInterface, { getSource: (obj) => obj.getSource(), // Git → SAP: Map abapGit values to ADK data (type inferred from AdkInterface) - fromAbapGit: ({ VSEOINTERF }) => ({ + fromAbapGit: ({ VSEOINTERF } = {}) => ({ name: (VSEOINTERF?.CLSNAME ?? '').toUpperCase(), type: 'INTF/OI', // ADT object type description: VSEOINTERF?.DESCRIPT, diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/prog.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/prog.ts index 01683287..383fbace 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/prog.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/prog.ts @@ -28,7 +28,7 @@ export const programHandler = createHandler(AdkProgram, { // Git → SAP: Map abapGit values to ADK data // Returns data matching AbapProgramSchema structure - fromAbapGit: ({ PROGDIR, TPOOL }) => { + fromAbapGit: ({ PROGDIR, TPOOL } = {}) => { // Extract description from TPOOL (text pool) if available // TPOOL.item can be a single object or an array let descriptionEntry: { ID?: string; ENTRY?: string } | undefined; diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts index 0f264619..1bc3268c 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts @@ -1,11 +1,149 @@ /** - * Table/Structure (TABL) object handler for abapGit format + * Table/Structure (TABL) object handlers for abapGit format + * + * Serializes SAP tables (TABL/DT) and structures (TABL/DS) to + * abapGit-compatible XML format by fetching the CDS-style source code + * from SAP, parsing it with @abapify/acds, and mapping the AST into + * DD02V/DD03P structures. + * + * Tables and structures share the same serialization logic but use + * different ADT endpoints (ddic/tables vs ddic/structures). + * + * Data sources: + * - blueSource GET: name, type, description, language + * - source/main GET: CDS source with annotations and field definitions + * → parsed via @abapify/acds into AST + * → mapped into DD02V annotations and DD03P field entries */ -import { AdkTable } from '../adk'; +import { + parse, + type TableDefinition, + type StructureDefinition, +} from '@abapify/acds'; +import { AdkTable, AdkStructure } from '../adk'; import { tabl } from '../../../schemas/generated'; import { createHandler } from '../base'; import { isoToSapLang, sapLangToIso } from '../lang'; +import { buildDD02V, buildDD03P } from '../cds-to-abapgit'; +import type { AdkObject } from '../adk'; + +/** + * Strip undefined/empty-string values from an object + * to avoid emitting empty XML elements + */ +function stripEmpty>(obj: T): Partial { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined && value !== '') { + result[key] = value; + } + } + return result as Partial; +} + +/** + * Shared serialize logic for tables and structures. + * Both use CDS source → DD02V/DD03P mapping. + */ +async function serializeTabl( + obj: AdkTable | AdkStructure, + ctx: { + getObjectName: (obj: AdkObject) => string; + toAbapGitXml: (obj: AdkObject) => string; + createFile: ( + path: string, + content: string, + ) => { path: string; content: string }; + }, +): Promise<{ path: string; content: string }[]> { + const objectName = ctx.getObjectName(obj); + const lang = isoToSapLang(obj.language || undefined); + + // Fetch CDS source from SAP + let cdsSource: string; + try { + cdsSource = await obj.getSource(); + } catch { + // Fallback: if source fetch fails, produce minimal DD02V only + const xmlContent = ctx.toAbapGitXml(obj); + return [ctx.createFile(`${objectName}.tabl.xml`, xmlContent)]; + } + + // Parse CDS source with @abapify/acds + const { ast, errors } = parse(cdsSource); + if (errors.length > 0 || ast.definitions.length === 0) { + // If parsing fails, fall back to minimal DD02V + const xmlContent = ctx.toAbapGitXml(obj); + return [ctx.createFile(`${objectName}.tabl.xml`, xmlContent)]; + } + + // Extract table/structure definition from AST + const def = ast.definitions[0] as TableDefinition | StructureDefinition; + + // Build DD02V from AST annotations + const dd02v = buildDD02V(def, lang, obj.description ?? ''); + + // Build DD03P from AST field definitions + const dd03pEntries = buildDD03P(def.members); + + // Construct the full abapGit values + const values: Record = { + DD02V: stripEmpty(dd02v), + }; + + if (dd03pEntries.length > 0) { + values.DD03P_TABLE = { + DD03P: dd03pEntries.map((entry) => stripEmpty(entry)), + }; + } + + // Build the full abapGit XML payload + const fullPayload = { + abap: { + version: '1.0', + values, + }, + version: 'v1.0.0', + serializer: 'LCL_OBJECT_TABL', + serializer_version: 'v1.0.0', + }; + + // Build XML using the schema + let xml = tabl.build(fullPayload as any, { pretty: true }); + + // Format attributes on separate lines for readability + xml = xml.replace( + /<([^\s>]+)((?:\s+[^\s=]+="[^"]*")+)\s*(\/?)>/g, + (match, tag, attrs, selfClose) => { + const attrList = attrs + .trim() + .split(/\s+(?=[^\s=]+=)/) + .map((a: string) => `\n ${a}`) + .join(''); + return `<${tag}${attrList}\n${selfClose ? '/' : ''}>`; + }, + ); + + // Move xmlns:asx from root to asx:abap element (abapGit format convention) + xml = xml.replace( + /(]*)\s+xmlns:asx="http:\/\/www\.sap\.com\/abapxml"([^>]*>[\s\S]*?)(; +} export const tableHandler = createHandler(AdkTable, { schema: tabl, @@ -17,18 +155,30 @@ export const tableHandler = createHandler(AdkTable, { DD02V: { TABNAME: obj.name ?? '', DDLANGUAGE: isoToSapLang(obj.language || undefined), - TABCLASS: (obj.dataSync as any)?.tabClass ?? '', + TABCLASS: 'TRANSP', + DDTEXT: obj.description ?? '', + }, + }), + + serialize: (obj, ctx) => serializeTabl(obj, ctx), + fromAbapGit: fromAbapGitTabl, +}); + +export const structureHandler = createHandler(AdkStructure, { + schema: tabl, + version: 'v1.0.0', + serializer: 'LCL_OBJECT_TABL', + serializer_version: 'v1.0.0', + + toAbapGit: (obj) => ({ + DD02V: { + TABNAME: obj.name ?? '', + DDLANGUAGE: isoToSapLang(obj.language || undefined), + TABCLASS: 'INTTAB', DDTEXT: obj.description ?? '', - EXCLASS: (obj.dataSync as any)?.exclass ?? '', }, }), - fromAbapGit: ({ DD02V }) => - ({ - name: (DD02V?.TABNAME ?? '').toUpperCase(), - type: DD02V?.TABCLASS === 'INTTAB' ? 'TABL/DS' : 'TABL/DT', - description: DD02V?.DDTEXT, - language: sapLangToIso(DD02V?.DDLANGUAGE), - masterLanguage: sapLangToIso(DD02V?.DDLANGUAGE), - }) as { name: string } & Record, + serialize: (obj, ctx) => serializeTabl(obj, ctx), + fromAbapGit: fromAbapGitTabl, }); diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/ttyp.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/ttyp.ts index ee7bf514..91989567 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/ttyp.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/ttyp.ts @@ -30,12 +30,25 @@ export const tableTypeHandler = createHandler(AdkTableType, { }; }, - fromAbapGit: ({ DD40V }) => + fromAbapGit: ({ DD40V } = {}) => ({ name: (DD40V?.TYPENAME ?? '').toUpperCase(), type: 'TTYP/TT', description: DD40V?.DDTEXT, language: sapLangToIso(DD40V?.DDLANGUAGE), masterLanguage: sapLangToIso(DD40V?.DDLANGUAGE), + rowType: { + typeName: DD40V?.ROWTYPE || undefined, + typeKind: DD40V?.ROWKIND || undefined, + builtInType: DD40V?.DATATYPE ? { dataType: DD40V.DATATYPE } : undefined, + }, + accessType: DD40V?.ACCESSMODE || undefined, + primaryKey: { + definition: DD40V?.KEYDEF || undefined, + kind: DD40V?.KEYKIND || undefined, + }, + secondaryKeys: { + allowed: 'N', + }, }) as { name: string } & Record, }); diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts index 34e0cee1..fe97c69e 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -165,8 +169,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts index 24f9b60e..b68690e0 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -80,8 +84,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts index 330b183c..20c3c0b7 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -195,8 +199,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts index 2004ecc7..7c2dc9e6 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -66,7 +70,7 @@ export default { }, { name: 'Dd04vType', - all: { + sequence: { element: [ { name: 'ROLLNAME', @@ -78,72 +82,77 @@ export default { minOccurs: '0', }, { - name: 'DDTEXT', + name: 'DOMNAME', type: 'xs:string', minOccurs: '0', }, { - name: 'DOMNAME', + name: 'HEADLEN', type: 'xs:string', minOccurs: '0', }, { - name: 'DATATYPE', + name: 'SCRLEN1', type: 'xs:string', minOccurs: '0', }, { - name: 'LENG', + name: 'SCRLEN2', type: 'xs:string', minOccurs: '0', }, { - name: 'DECIMALS', + name: 'SCRLEN3', type: 'xs:string', minOccurs: '0', }, { - name: 'HEADLEN', + name: 'DDTEXT', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRLEN1', + name: 'REPTEXT', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRLEN2', + name: 'SCRTEXT_S', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRLEN3', + name: 'SCRTEXT_M', type: 'xs:string', minOccurs: '0', }, { - name: 'REPTEXT', + name: 'SCRTEXT_L', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRTEXT_S', + name: 'DTELMASTER', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRTEXT_M', + name: 'DATATYPE', type: 'xs:string', minOccurs: '0', }, { - name: 'SCRTEXT_L', + name: 'LENG', type: 'xs:string', minOccurs: '0', }, { - name: 'DTELMASTER', + name: 'DECIMALS', + type: 'xs:string', + minOccurs: '0', + }, + { + name: 'OUTPUTLEN', type: 'xs:string', minOccurs: '0', }, @@ -152,6 +161,11 @@ export default { type: 'xs:string', minOccurs: '0', }, + { + name: 'REFTYPE', + type: 'xs:string', + minOccurs: '0', + }, { name: 'ABAP_LANGUAGE_VERSION', type: 'xs:string', @@ -165,8 +179,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts index b0efc5ef..e2daeb08 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts @@ -105,8 +105,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts index 70eeeaaf..aa7f12eb 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -110,8 +114,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts index 02634b60..847c9e51 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts @@ -100,8 +100,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts index cd187c61..62320c52 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -87,6 +91,11 @@ export default { type: 'xs:string', minOccurs: '0', }, + { + name: 'CLIDEP', + type: 'xs:string', + minOccurs: '0', + }, { name: 'SQLTAB', type: 'xs:string', @@ -98,7 +107,7 @@ export default { minOccurs: '0', }, { - name: 'BUFFERED', + name: 'DDTEXT', type: 'xs:string', minOccurs: '0', }, @@ -108,27 +117,27 @@ export default { minOccurs: '0', }, { - name: 'MATEFLAG', + name: 'BUFFERED', type: 'xs:string', minOccurs: '0', }, { - name: 'CONTFLAG', + name: 'MATEFLAG', type: 'xs:string', minOccurs: '0', }, { - name: 'SHLPEXI', + name: 'CONTFLAG', type: 'xs:string', minOccurs: '0', }, { - name: 'EXCLASS', + name: 'SHLPEXI', type: 'xs:string', minOccurs: '0', }, { - name: 'DDTEXT', + name: 'EXCLASS', type: 'xs:string', minOccurs: '0', }, @@ -190,22 +199,22 @@ export default { minOccurs: '0', }, { - name: 'DATATYPE', + name: 'NOTNULL', type: 'xs:string', minOccurs: '0', }, { - name: 'LENG', + name: 'DATATYPE', type: 'xs:string', minOccurs: '0', }, { - name: 'DECIMALS', + name: 'LENG', type: 'xs:string', minOccurs: '0', }, { - name: 'NOTNULL', + name: 'DECIMALS', type: 'xs:string', minOccurs: '0', }, @@ -220,12 +229,12 @@ export default { minOccurs: '0', }, { - name: 'COMPTYPE', + name: 'MASK', type: 'xs:string', minOccurs: '0', }, { - name: 'MASK', + name: 'COMPTYPE', type: 'xs:string', minOccurs: '0', }, @@ -275,8 +284,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts index 60a0ce3d..37470369 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts @@ -46,6 +46,10 @@ export default { name: 'Schema', abstract: true, }, + { + name: 'values', + type: 'asx:AbapValuesType', + }, { name: 'abap', type: 'asx:AbapType', @@ -145,8 +149,7 @@ export default { sequence: { element: [ { - name: 'values', - type: 'asx:AbapValuesType', + ref: 'asx:values', }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts index 5c663d5e..5bf63c7e 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts @@ -38,6 +38,30 @@ export type ClasSchema = serializer_version: string; }; } + | { + values: { + VSEOCLASS?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + STATE?: string; + CATEGORY?: string; + EXPOSURE?: string; + CLSFINAL?: string; + CLSABSTRCT?: string; + CLSCCINCL?: string; + FIXPT?: string; + UNICODE?: string; + WITH_UNIT_TESTS?: string; + DURATION?: string; + RISK?: string; + MSG_ID?: string; + REFCLSNAME?: string; + SHRM_ENABLED?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + }; + } | { abap: { values: { diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts index 03dacb17..ccd3eb36 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts @@ -21,6 +21,13 @@ export type DevcSchema = serializer_version: string; }; } + | { + values: { + DEVC?: { + CTEXT: string; + }; + }; + } | { abap: { values: { diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts index b9f994af..ecd26beb 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts @@ -43,6 +43,35 @@ export type DomaSchema = serializer_version: string; }; } + | { + values: { + DD01V?: { + DOMNAME: string; + DDLANGUAGE?: string; + DATATYPE?: string; + LENG?: string; + OUTPUTLEN?: string; + DECIMALS?: string; + LOWERCASE?: string; + SIGNFLAG?: string; + VALEXI?: string; + ENTITYTAB?: string; + CONVEXIT?: string; + DDTEXT?: string; + DOMMASTER?: string; + }; + DD07V_TAB?: { + DD07V?: { + DOMNAME?: string; + VALPOS?: string; + DDLANGUAGE?: string; + DOMVALUE_L?: string; + DOMVALUE_H?: string; + DDTEXT?: string; + }[]; + }; + }; + } | { abap: { values: { diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts index 0f91a76f..febce0dc 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts @@ -13,21 +13,23 @@ export type DtelSchema = DD04V?: { ROLLNAME: string; DDLANGUAGE?: string; - DDTEXT?: string; DOMNAME?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; HEADLEN?: string; SCRLEN1?: string; SCRLEN2?: string; SCRLEN3?: string; + DDTEXT?: string; REPTEXT?: string; SCRTEXT_S?: string; SCRTEXT_M?: string; SCRTEXT_L?: string; DTELMASTER?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + OUTPUTLEN?: string; REFKIND?: string; + REFTYPE?: string; ABAP_LANGUAGE_VERSION?: string; }; }; @@ -38,27 +40,55 @@ export type DtelSchema = serializer_version: string; }; } + | { + values: { + DD04V?: { + ROLLNAME: string; + DDLANGUAGE?: string; + DOMNAME?: string; + HEADLEN?: string; + SCRLEN1?: string; + SCRLEN2?: string; + SCRLEN3?: string; + DDTEXT?: string; + REPTEXT?: string; + SCRTEXT_S?: string; + SCRTEXT_M?: string; + SCRTEXT_L?: string; + DTELMASTER?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + OUTPUTLEN?: string; + REFKIND?: string; + REFTYPE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + }; + } | { abap: { values: { DD04V?: { ROLLNAME: string; DDLANGUAGE?: string; - DDTEXT?: string; DOMNAME?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; HEADLEN?: string; SCRLEN1?: string; SCRLEN2?: string; SCRLEN3?: string; + DDTEXT?: string; REPTEXT?: string; SCRTEXT_S?: string; SCRTEXT_M?: string; SCRTEXT_L?: string; DTELMASTER?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + OUTPUTLEN?: string; REFKIND?: string; + REFTYPE?: string; ABAP_LANGUAGE_VERSION?: string; }; }; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts index 25024e7d..e56597a8 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts @@ -27,6 +27,19 @@ export type IntfSchema = serializer_version: string; }; } + | { + values: { + VSEOINTERF?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + EXPOSURE?: string; + STATE?: string; + UNICODE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + }; + } | { abap: { values: { diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts index 58c48cf4..e8b62361 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts @@ -14,15 +14,16 @@ export type TablSchema = TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; + CLIDEP?: string; SQLTAB?: string; DATCLASS?: string; - BUFFERED?: string; + DDTEXT?: string; MASTERLANG?: string; + BUFFERED?: string; MATEFLAG?: string; CONTFLAG?: string; SHLPEXI?: string; EXCLASS?: string; - DDTEXT?: string; AUTHCLASS?: string; }; DD03P_TABLE?: { @@ -36,14 +37,14 @@ export type TablSchema = ADMINFIELD?: string; INTTYPE?: string; INTLEN?: string; + NOTNULL?: string; DATATYPE?: string; LENG?: string; DECIMALS?: string; - NOTNULL?: string; DOMNAME?: string; SHLPORIGIN?: string; - COMPTYPE?: string; MASK?: string; + COMPTYPE?: string; REFTABLE?: string; REFFIELD?: string; CONRFLAG?: string; @@ -59,6 +60,52 @@ export type TablSchema = serializer_version: string; }; } + | { + values: { + DD02V?: { + TABNAME: string; + DDLANGUAGE?: string; + TABCLASS?: string; + CLIDEP?: string; + SQLTAB?: string; + DATCLASS?: string; + DDTEXT?: string; + MASTERLANG?: string; + BUFFERED?: string; + MATEFLAG?: string; + CONTFLAG?: string; + SHLPEXI?: string; + EXCLASS?: string; + AUTHCLASS?: string; + }; + DD03P_TABLE?: { + DD03P?: { + TABNAME?: string; + FIELDNAME?: string; + DDLANGUAGE?: string; + POSITION?: string; + KEYFLAG?: string; + ROLLNAME?: string; + ADMINFIELD?: string; + INTTYPE?: string; + INTLEN?: string; + NOTNULL?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + DOMNAME?: string; + SHLPORIGIN?: string; + MASK?: string; + COMPTYPE?: string; + REFTABLE?: string; + REFFIELD?: string; + CONRFLAG?: string; + PRECFIELD?: string; + DDTEXT?: string; + }[]; + }; + }; + } | { abap: { values: { @@ -66,15 +113,16 @@ export type TablSchema = TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; + CLIDEP?: string; SQLTAB?: string; DATCLASS?: string; - BUFFERED?: string; + DDTEXT?: string; MASTERLANG?: string; + BUFFERED?: string; MATEFLAG?: string; CONTFLAG?: string; SHLPEXI?: string; EXCLASS?: string; - DDTEXT?: string; AUTHCLASS?: string; }; DD03P_TABLE?: { @@ -88,14 +136,14 @@ export type TablSchema = ADMINFIELD?: string; INTTYPE?: string; INTLEN?: string; + NOTNULL?: string; DATATYPE?: string; LENG?: string; DECIMALS?: string; - NOTNULL?: string; DOMNAME?: string; SHLPORIGIN?: string; - COMPTYPE?: string; MASK?: string; + COMPTYPE?: string; REFTABLE?: string; REFFIELD?: string; CONRFLAG?: string; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts index f73d6c33..c3e142c7 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts @@ -34,6 +34,26 @@ export type TtypSchema = serializer_version: string; }; } + | { + values: { + DD40V?: { + TYPENAME: string; + DDLANGUAGE?: string; + ROWTYPE?: string; + ROWKIND?: string; + DATATYPE?: string; + ACCESSMODE?: string; + KEYDEF?: string; + KEYKIND?: string; + GENERIC?: string; + LENG?: string; + DECIMALS?: string; + DDTEXT?: string; + TYPELEN?: string; + DEFFDNAME?: string; + }; + }; + } | { abap: { values: { diff --git a/packages/adt-plugin-abapgit/tests/handlers/dtel-e2e.test.ts b/packages/adt-plugin-abapgit/tests/handlers/dtel-e2e.test.ts new file mode 100644 index 00000000..458d3905 --- /dev/null +++ b/packages/adt-plugin-abapgit/tests/handlers/dtel-e2e.test.ts @@ -0,0 +1,267 @@ +/** + * Tests for DTEL end-to-end: SAP XML response → schema parse → handler serialize + * + * This tests the full data flow that happens during roundtrip Phase 2: + * 1. SAP returns XML response for GET /sap/bc/adt/ddic/dataelements/{name} + * 2. The dataelementWrapper schema parses it into JS object + * 3. The wbobj content becomes the ADK object's dataSync + * 4. The DTEL handler maps dataSync → abapGit DD04V XML + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +import { parseXml, type Schema } from '@abapify/ts-xsd'; + +// Import the raw schema literals (these are the generated schema JS objects) +// The dataelementWrapper imports adtcore + dataelements schemas +import dataelementWrapperSchema from '../../../adt-schemas/src/schemas/generated/schemas/custom/dataelementWrapper.ts'; +import adtcoreSchema from '../../../adt-schemas/src/schemas/generated/schemas/sap/adtcore.ts'; +import dataelementsSchema from '../../../adt-schemas/src/schemas/generated/schemas/sap/dataelements.ts'; + +// Import handler to trigger registration +import '../../src/lib/handlers/objects/dtel.ts'; +import { getHandler } from '../../src/lib/handlers/base.ts'; + +/** + * Parse SAP DTEL XML response using the dataelementWrapper schema + * Replicates what the adt-client adapter does at runtime + */ +function parseDtelResponse(xml: string): any { + // The schema needs its $imports resolved - we do this manually since + // we're not using the TypedSchema wrapper + const schema = { + ...dataelementWrapperSchema, + $imports: [adtcoreSchema, dataelementsSchema], + }; + return parseXml(schema as unknown as Schema, xml); +} + +/** + * Mock SAP ADT XML response for a domain-based data element + * + * This is what SAP returns from GET /sap/bc/adt/ddic/dataelements/ztest_dtel + * with Accept: application/vnd.sap.adt.dataelements.v2+xml + */ +const SAP_DTEL_RESPONSE_DOMAIN = ` + + + + domain + ZTEST_DOMAIN + + 0 + false + 0 + false + Short + 10 + 10 + Medium Text + 20 + 20 + Long Description + 40 + 40 + Heading Text + 55 + 55 + +`; + +/** + * Mock SAP ADT XML response for a predefined-type data element + */ +const SAP_DTEL_RESPONSE_PREDEFINED = ` + + + + predefinedAbapType + + CHAR + 10 + true + 0 + false + Short + 10 + 10 + Medium + 20 + 20 + Long Text + 40 + 40 + Head + 55 + 55 + +`; + +describe('DTEL end-to-end: SAP XML → schema parse → handler serialize', () => { + it('parses SAP XML response and extracts dataElement', () => { + const parsed = parseDtelResponse(SAP_DTEL_RESPONSE_DOMAIN); + + // Verify root structure + assert.ok(parsed.wbobj, 'Should have wbobj root'); + assert.strictEqual(parsed.wbobj.name, 'ZTEST_DTEL_DOMAIN'); + assert.strictEqual(parsed.wbobj.type, 'DTEL/DE'); + assert.strictEqual(parsed.wbobj.description, 'Domain-based data element'); + assert.strictEqual(parsed.wbobj.language, 'EN'); + assert.strictEqual(parsed.wbobj.masterLanguage, 'EN'); + + // CRITICAL: Verify nested dataElement is present + assert.ok( + parsed.wbobj.dataElement, + 'dataElement should be present in parsed response', + ); + assert.strictEqual( + parsed.wbobj.dataElement!.typeKind, + 'domain', + 'typeKind should be domain', + ); + assert.strictEqual( + parsed.wbobj.dataElement!.typeName, + 'ZTEST_DOMAIN', + 'typeName should be ZTEST_DOMAIN', + ); + assert.strictEqual( + parsed.wbobj.dataElement!.shortFieldLabel, + 'Short', + 'shortFieldLabel should be Short', + ); + assert.strictEqual( + parsed.wbobj.dataElement!.headingFieldLength, + 55, + 'headingFieldLength should be 55', + ); + }); + + it('full chain: SAP XML → parse → mock ADK object → handler serialize', async () => { + // Step 1: Parse SAP response (this is what the adapter does) + const parsed = parseDtelResponse(SAP_DTEL_RESPONSE_DOMAIN); + const wbobj = parsed.wbobj; + + // Step 2: Create mock ADK object using parsed data (this is what load() does) + const mockAdkObject = { + name: wbobj.name, + type: wbobj.type, + kind: 'DataElement', + description: wbobj.description ?? '', + language: wbobj.language ?? '', + masterLanguage: wbobj.masterLanguage ?? '', + abapLanguageVersion: wbobj.abapLanguageVersion ?? '', + dataSync: wbobj, + }; + + // Step 3: Serialize using handler (this is what format.import does) + const handler = getHandler('DTEL'); + assert.ok(handler, 'DTEL handler should be registered'); + + const files = await handler!.serialize(mockAdkObject as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + assert.ok(xmlFile, 'Should produce .dtel.xml file'); + + const xml = xmlFile!.content; + + // Verify ALL key fields are populated (not empty) + assert.ok( + xml.includes('ZTEST_DTEL_DOMAIN'), + `ROLLNAME should be set. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Domain-based data element'), + `DDTEXT should be set. XML:\n${xml}`, + ); + assert.ok( + xml.includes('ZTEST_DOMAIN'), + `DOMNAME should be set from dataElement.typeName. XML:\n${xml}`, + ); + assert.ok( + xml.includes('E'), + `DDLANGUAGE should be E (mapped from EN). XML:\n${xml}`, + ); + assert.ok( + xml.includes('E'), + `DTELMASTER should be E (mapped from EN). XML:\n${xml}`, + ); + assert.ok( + xml.includes('D'), + `REFKIND should be D for domain. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Short'), + `SCRTEXT_S should be set. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Medium Text'), + `SCRTEXT_M should be set. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Long Description'), + `SCRTEXT_L should be set. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Heading Text'), + `REPTEXT should be set. XML:\n${xml}`, + ); + }); + + it('full chain with predefined type', async () => { + const parsed = parseDtelResponse(SAP_DTEL_RESPONSE_PREDEFINED); + const wbobj = parsed.wbobj; + + const mockAdkObject = { + name: wbobj.name, + type: wbobj.type, + kind: 'DataElement', + description: wbobj.description ?? '', + language: wbobj.language ?? '', + masterLanguage: wbobj.masterLanguage ?? '', + abapLanguageVersion: wbobj.abapLanguageVersion ?? '', + dataSync: wbobj, + }; + + const handler = getHandler('DTEL'); + const files = await handler!.serialize(mockAdkObject as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + assert.ok( + xml.includes('CHAR'), + `DATATYPE should be CHAR. XML:\n${xml}`, + ); + assert.ok( + xml.includes('000010'), + `LENG should be zero-padded 10. XML:\n${xml}`, + ); + assert.ok( + !xml.includes('D'), + 'REFKIND should NOT be D for predefined type', + ); + }); +}); diff --git a/packages/adt-plugin-abapgit/tests/handlers/dtel.test.ts b/packages/adt-plugin-abapgit/tests/handlers/dtel.test.ts new file mode 100644 index 00000000..bd5ebffb --- /dev/null +++ b/packages/adt-plugin-abapgit/tests/handlers/dtel.test.ts @@ -0,0 +1,480 @@ +/** + * Tests for DTEL handler toAbapGit mapping + * + * Verifies that ADK DataElement data is correctly mapped to abapGit DD04V fields. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +// Import handler to trigger registration +import '../../src/lib/handlers/objects/dtel.ts'; +import { getHandler } from '../../src/lib/handlers/base.ts'; + +/** + * Create a mock ADK DataElement object that mimics what SAP returns after load() + * + * The dataSync structure matches DataelementWrapperSchema['wbobj']: + * - Flat AdtMainObject fields: name, type, description, language, masterLanguage, etc. + * - Nested dataElement: typeKind, typeName, dataType, labels, etc. + */ +function createMockDtel(overrides?: { + name?: string; + description?: string; + language?: string; + masterLanguage?: string; + abapLanguageVersion?: string; + dataElement?: Record; +}) { + const name = overrides?.name ?? 'ZTEST_DTEL'; + const description = overrides?.description ?? 'Test Data Element'; + const language = overrides?.language ?? 'EN'; + const masterLanguage = overrides?.masterLanguage ?? 'EN'; + const abapLanguageVersion = overrides?.abapLanguageVersion ?? ''; + + const dataElement = overrides?.dataElement ?? { + typeKind: 'domain', + typeName: 'ZTEST_DOMAIN', + dataType: '', + dataTypeLength: 0, + dataTypeLengthEnabled: false, + dataTypeDecimals: 0, + dataTypeDecimalsEnabled: false, + shortFieldLabel: 'Short', + shortFieldLength: 10, + shortFieldMaxLength: 10, + mediumFieldLabel: 'Medium Text', + mediumFieldLength: 20, + mediumFieldMaxLength: 20, + longFieldLabel: 'Long Text Label', + longFieldLength: 40, + longFieldMaxLength: 40, + headingFieldLabel: 'Heading', + headingFieldLength: 55, + headingFieldMaxLength: 55, + searchHelp: '', + searchHelpParameter: '', + setGetParameter: '', + defaultComponentName: '', + deactivateInputHistory: false, + changeDocument: false, + leftToRightDirection: false, + deactivateBIDIFiltering: false, + }; + + // The mock mimics the real ADK object after load() + // dataSync returns the wbobj content, base properties use getters + const data = { + name, + type: 'DTEL/DE', + description, + language, + masterLanguage, + abapLanguageVersion, + dataElement, + }; + + return { + // Base class properties (implemented as getters in real ADK) + name, + type: 'DTEL/DE', + kind: 'DataElement', + description, + language, + masterLanguage, + abapLanguageVersion, + // dataSync returns the full wbobj data + dataSync: data, + }; +} + +describe('DTEL handler toAbapGit', () => { + const handler = getHandler('DTEL'); + + it('handler is registered', () => { + assert.ok(handler, 'DTEL handler should be registered'); + assert.strictEqual(handler!.type, 'DTEL'); + }); + + it('serializes domain-based data element correctly', async () => { + const mock = createMockDtel(); + const files = await handler!.serialize(mock as any); + + assert.ok(files.length >= 1, 'Should produce at least one file'); + + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + assert.ok(xmlFile, 'Should produce a .dtel.xml file'); + + const xml = xmlFile!.content; + + // Check envelope attributes + assert.ok(xml.includes('LCL_OBJECT_DTEL'), 'Should have serializer name'); + assert.ok( + xml.includes('serializer_version'), + 'Should have serializer_version', + ); + + // Check DD04V fields derived from obj.name / obj.description + assert.ok( + xml.includes('ZTEST_DTEL'), + `ROLLNAME should be object name. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Test Data Element'), + `DDTEXT should be description. XML:\n${xml}`, + ); + + // Check DD04V fields derived from dataElement + assert.ok( + xml.includes('ZTEST_DOMAIN'), + `DOMNAME should be typeName from dataElement. XML:\n${xml}`, + ); + assert.ok( + xml.includes('D'), + `REFKIND should be D for domain typeKind. XML:\n${xml}`, + ); + + // Check language mapping (ISO EN → SAP E) + assert.ok( + xml.includes('E'), + `DDLANGUAGE should be SAP lang code. XML:\n${xml}`, + ); + assert.ok( + xml.includes('E'), + `DTELMASTER should be SAP lang code. XML:\n${xml}`, + ); + + // Check field labels + assert.ok( + xml.includes('Heading'), + `REPTEXT should be headingFieldLabel. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Short'), + `SCRTEXT_S should be shortFieldLabel. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Medium Text'), + `SCRTEXT_M should be mediumFieldLabel. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Long Text Label'), + `SCRTEXT_L should be longFieldLabel. XML:\n${xml}`, + ); + + // Check field lengths + assert.ok( + xml.includes('55'), + `HEADLEN should be headingFieldLength. XML:\n${xml}`, + ); + assert.ok( + xml.includes('10'), + `SCRLEN1 should be shortFieldLength. XML:\n${xml}`, + ); + assert.ok( + xml.includes('20'), + `SCRLEN2 should be mediumFieldLength. XML:\n${xml}`, + ); + assert.ok( + xml.includes('40'), + `SCRLEN3 should be longFieldLength. XML:\n${xml}`, + ); + + // Domain-based DTELs should NOT have DATATYPE/LENG/OUTPUTLEN + // (those are inherited from the domain) + assert.ok( + !xml.includes(' { + const mock = createMockDtel({ + name: 'ZTEST_PREDEFINED', + description: 'Predefined Type Element', + dataElement: { + typeKind: 'predefinedAbapType', + typeName: '', + dataType: 'CHAR', + dataTypeLength: 10, + dataTypeLengthEnabled: true, + dataTypeDecimals: 0, + dataTypeDecimalsEnabled: false, + shortFieldLabel: 'Short', + shortFieldLength: 10, + mediumFieldLabel: 'Medium', + mediumFieldLength: 20, + longFieldLabel: 'Long', + longFieldLength: 40, + headingFieldLabel: 'Head', + headingFieldLength: 55, + }, + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + assert.ok( + xml.includes('ZTEST_PREDEFINED'), + 'ROLLNAME should be set', + ); + assert.ok( + xml.includes('CHAR'), + `DATATYPE should be CHAR. XML:\n${xml}`, + ); + assert.ok( + xml.includes('000010'), + `LENG should be zero-padded. XML:\n${xml}`, + ); + + // predefinedAbapType → REFKIND empty (not 'D') + assert.ok( + !xml.includes('D'), + 'REFKIND should NOT be D for predefined type', + ); + + // OUTPUTLEN should be derived for CHAR(10) → 000010 + assert.ok( + xml.includes('000010'), + `OUTPUTLEN should be derived for CHAR type. XML:\n${xml}`, + ); + + // Empty typeName → DOMNAME should be omitted entirely + assert.ok( + !xml.includes(' { + const mock = createMockDtel({ + name: 'ZTEST_REF', + description: 'Ref to CLAS', + dataElement: { + typeKind: 'refToClifType', + typeName: 'ZCL_SOME_CLASS', + shortFieldLabel: 'Ref', + shortFieldLength: 10, + mediumFieldLabel: 'Reference', + mediumFieldLength: 20, + longFieldLabel: 'Class Reference', + longFieldLength: 40, + headingFieldLabel: 'ClsRef', + headingFieldLength: 20, + }, + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + assert.ok( + xml.includes('ZCL_SOME_CLASS'), + `DOMNAME should be typeName for ref types. XML:\n${xml}`, + ); + assert.ok( + xml.includes('R'), + `REFKIND should be R for ref types. XML:\n${xml}`, + ); + assert.ok( + xml.includes('C'), + `REFTYPE should be C for refToClifType. XML:\n${xml}`, + ); + + // Reference types should emit DATATYPE=REF (abapGit convention) + assert.ok( + xml.includes('REF'), + `DATATYPE should be REF for reference types. XML:\n${xml}`, + ); + }); + + it('handles missing dataElement gracefully', async () => { + // Simulate case where SAP response has no dataElement + const mock = createMockDtel({ + name: 'ZTEST_EMPTY', + description: 'Empty DTEL', + dataElement: undefined as any, + }); + + // Should not throw + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + assert.ok(xmlFile, 'Should still produce XML file'); + + const xml = xmlFile!.content; + // Base fields should still be present + assert.ok( + xml.includes('ZTEST_EMPTY'), + `ROLLNAME should still be set from obj.name. XML:\n${xml}`, + ); + assert.ok( + xml.includes('Empty DTEL'), + `DDTEXT should still be set from obj.description. XML:\n${xml}`, + ); + }); + + it('does not emit ABAP_LANGUAGE_VERSION', async () => { + const mock = createMockDtel({ + name: 'ZTEST_ALV', + description: 'Cloud Element', + abapLanguageVersion: 'cloudDevelopment', + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + assert.ok( + !xml.includes(' { + const mock = createMockDtel({ + name: 'ZTEST_DEC', + description: 'Decimal Element', + dataElement: { + typeKind: 'predefinedAbapType', + typeName: '', + dataType: 'DEC', + dataTypeLength: 13, + dataTypeLengthEnabled: true, + dataTypeDecimals: 2, + dataTypeDecimalsEnabled: true, + shortFieldLabel: 'Dec', + shortFieldLength: 10, + mediumFieldLabel: 'Decimal', + mediumFieldLength: 20, + longFieldLabel: 'Decimal Value', + longFieldLength: 40, + headingFieldLabel: 'DecVal', + headingFieldLength: 20, + }, + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + // DEC(13,2): intDigits=11, separators=3, outputLen=13+1+3=17 + assert.ok( + xml.includes('000017'), + `OUTPUTLEN for DEC(13,2) should be 000017. XML:\n${xml}`, + ); + assert.ok( + xml.includes('000002'), + `DECIMALS should be zero-padded. XML:\n${xml}`, + ); + }); + + it('derives OUTPUTLEN for INT4 type', async () => { + const mock = createMockDtel({ + name: 'ZTEST_INT', + description: 'Integer Element', + dataElement: { + typeKind: 'predefinedAbapType', + typeName: '', + dataType: 'INT4', + dataTypeLength: 10, + dataTypeLengthEnabled: false, + dataTypeDecimals: 0, + dataTypeDecimalsEnabled: false, + shortFieldLabel: 'Int', + shortFieldLength: 10, + mediumFieldLabel: 'Integer', + mediumFieldLength: 20, + longFieldLabel: 'Integer Value', + longFieldLength: 40, + headingFieldLabel: 'IntVal', + headingFieldLength: 20, + }, + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + // INT4 always has OUTPUTLEN 000010 + assert.ok( + xml.includes('000010'), + `OUTPUTLEN for INT4 should be 000010. XML:\n${xml}`, + ); + }); + + it('omits OUTPUTLEN for domain-based data elements', async () => { + const mock = createMockDtel({ + name: 'ZTEST_DOM', + description: 'Domain Element', + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + // Domain-based DTELs have no dataType → OUTPUTLEN should be absent + assert.ok( + !xml.includes(' { + const mock = createMockDtel({ + name: 'ZTEST_ORDER', + description: 'Order Test', + dataElement: { + typeKind: 'predefinedAbapType', + typeName: '', + dataType: 'CHAR', + dataTypeLength: 20, + dataTypeLengthEnabled: true, + dataTypeDecimals: 0, + dataTypeDecimalsEnabled: false, + shortFieldLabel: 'Short', + shortFieldLength: 10, + mediumFieldLabel: 'Medium', + mediumFieldLength: 20, + longFieldLabel: 'Long Text', + longFieldLength: 40, + headingFieldLabel: 'Heading', + headingFieldLength: 55, + }, + }); + + const files = await handler!.serialize(mock as any); + const xmlFile = files.find((f) => f.path.endsWith('.dtel.xml')); + const xml = xmlFile!.content; + + // Verify canonical order: ROLLNAME before DDLANGUAGE before HEADLEN ... + const rollnameIdx = xml.indexOf(''); + const ddlanguageIdx = xml.indexOf(''); + const headlenIdx = xml.indexOf(''); + const scrlen1Idx = xml.indexOf(''); + const ddtextIdx = xml.indexOf(''); + const reptextIdx = xml.indexOf(''); + const dtelmasterIdx = xml.indexOf(''); + const datatypeIdx = xml.indexOf(''); + const lengIdx = xml.indexOf(''); + const outputlenIdx = xml.indexOf(''); + + assert.ok(rollnameIdx < ddlanguageIdx, 'ROLLNAME before DDLANGUAGE'); + assert.ok(ddlanguageIdx < headlenIdx, 'DDLANGUAGE before HEADLEN'); + assert.ok(headlenIdx < scrlen1Idx, 'HEADLEN before SCRLEN1'); + assert.ok(scrlen1Idx < ddtextIdx, 'SCRLEN1 before DDTEXT'); + assert.ok(ddtextIdx < reptextIdx, 'DDTEXT before REPTEXT'); + assert.ok(reptextIdx < dtelmasterIdx, 'REPTEXT before DTELMASTER'); + assert.ok(dtelmasterIdx < datatypeIdx, 'DTELMASTER before DATATYPE'); + assert.ok(datatypeIdx < lengIdx, 'DATATYPE before LENG'); + assert.ok(lengIdx < outputlenIdx, 'LENG before OUTPUTLEN'); + }); +}); diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts new file mode 100644 index 00000000..d310863a --- /dev/null +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -0,0 +1,372 @@ +/** + * Tests for TABL handler + * + * Verifies that CDS source is correctly parsed via @abapify/acds + * and mapped to abapGit DD02V/DD03P XML format. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +// Import handler to trigger registration +import '../../src/lib/handlers/objects/tabl.ts'; +import { getHandler } from '../../src/lib/handlers/base.ts'; +import { + buildDD02V, + buildDD03P, +} from '../../src/lib/handlers/cds-to-abapgit.ts'; +import { parse } from '@abapify/acds'; + +// ============================================ +// CDS Source Fixtures +// ============================================ + +/** Structure with mixed field types (matches zage_structure.tabl.xml) */ +const CDS_STRUCTURE = ` +@EndUserText.label : 'AGE Test Structure' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +define structure zage_structure { + partner_id : abap.char(10); + description : abap.char(40); + amount : abap.curr(15,2); +} +`; + +/** Transparent table with keys and delivery class (matches zage_transparent_table.tabl.xml) */ +const CDS_TRANSPARENT_TABLE = ` +@EndUserText.label : 'AGE Test Transparent Table' +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #A +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +define table zage_transparent_table { + key mandt : mandt not null; + key key_field : abap.char(10) not null; + value_field : abap.char(40); +} +`; + +/** Table with data element references */ +const CDS_TABLE_DATA_ELEMENTS = ` +@EndUserText.label : 'Table with data elements' +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #C +define table ztable_dtel { + key client : abap.clnt not null; + key bukrs : bukrs not null; + name1 : name1_gp; +} +`; + +// ============================================ +// Mock Factory +// ============================================ + +function createMockTable(overrides?: { + name?: string; + type?: string; + description?: string; + language?: string; + cdsSource?: string; +}) { + const name = overrides?.name ?? 'ZTEST_TABLE'; + const type = overrides?.type ?? 'TABL/DT'; + const description = overrides?.description ?? 'Test Table'; + const language = overrides?.language ?? 'EN'; + const cdsSource = overrides?.cdsSource ?? CDS_TRANSPARENT_TABLE; + + return { + name, + type, + kind: 'Table' as const, + description, + language, + masterLanguage: language, + abapLanguageVersion: '', + dataSync: { name, type, description, language, masterLanguage: language }, + getSource: async () => cdsSource, + }; +} + +// ============================================ +// Tests +// ============================================ + +describe('TABL handler', () => { + const handler = getHandler('TABL'); + + it('handler is registered', () => { + assert.ok(handler, 'TABL handler should be registered'); + assert.strictEqual(handler!.type, 'TABL'); + }); + + describe('serialize — structure', () => { + it('produces DD02V with TABCLASS=INTTAB', async () => { + const mock = createMockTable({ + name: 'ZAGE_STRUCTURE', + type: 'TABL/DS', + description: 'AGE Test Structure', + cdsSource: CDS_STRUCTURE, + }); + + const files = await handler!.serialize(mock as any); + assert.strictEqual(files.length, 1); + + const xml = files[0].content; + assert.ok(xml.includes('ZAGE_STRUCTURE')); + assert.ok(xml.includes('INTTAB')); + assert.ok(xml.includes('AGE Test Structure')); + assert.ok(xml.includes('1')); + }); + + it('produces DD03P entries for all fields', async () => { + const mock = createMockTable({ + name: 'ZAGE_STRUCTURE', + type: 'TABL/DS', + description: 'AGE Test Structure', + cdsSource: CDS_STRUCTURE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + // PARTNER_ID — abap.char(10) + assert.ok(xml.includes('PARTNER_ID')); + assert.ok(xml.includes('CHAR')); + + // DESCRIPTION — abap.char(40) + assert.ok(xml.includes('DESCRIPTION')); + + // AMOUNT — abap.curr(15,2) + assert.ok(xml.includes('AMOUNT')); + assert.ok(xml.includes('CURR')); + assert.ok(xml.includes('P')); + }); + + it('sets POSITION for fields', async () => { + const mock = createMockTable({ + name: 'ZAGE_STRUCTURE', + type: 'TABL/DS', + description: 'AGE Test Structure', + cdsSource: CDS_STRUCTURE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + // All fields should have POSITION + assert.ok(xml.includes('0001')); + assert.ok(xml.includes('0002')); + assert.ok(xml.includes('0003')); + }); + }); + + describe('serialize — transparent table', () => { + it('produces DD02V with TABCLASS=TRANSP and CONTFLAG', async () => { + const mock = createMockTable({ + name: 'ZAGE_TRANSPARENT_TABLE', + description: 'AGE Test Transparent Table', + cdsSource: CDS_TRANSPARENT_TABLE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + assert.ok(xml.includes('TRANSP')); + assert.ok(xml.includes('A')); + assert.ok(xml.includes('1')); + }); + + it('marks key fields with KEYFLAG', async () => { + const mock = createMockTable({ + name: 'ZAGE_TRANSPARENT_TABLE', + description: 'AGE Test Transparent Table', + cdsSource: CDS_TRANSPARENT_TABLE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + // Should have KEYFLAG for key fields + assert.ok(xml.includes('X')); + }); + + it('marks not null fields with NOTNULL', async () => { + const mock = createMockTable({ + name: 'ZAGE_TRANSPARENT_TABLE', + description: 'AGE Test Transparent Table', + cdsSource: CDS_TRANSPARENT_TABLE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + assert.ok(xml.includes('X')); + }); + + it('handles data element reference (mandt)', async () => { + const mock = createMockTable({ + name: 'ZAGE_TRANSPARENT_TABLE', + description: 'AGE Test Transparent Table', + cdsSource: CDS_TRANSPARENT_TABLE, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + // MANDT field references data element + assert.ok(xml.includes('MANDT')); + assert.ok(xml.includes('E')); + }); + + it('detects client-dependent table', async () => { + const mock = createMockTable({ + name: 'ZTABLE_DTEL', + description: 'Table with data elements', + cdsSource: CDS_TABLE_DATA_ELEMENTS, + }); + + const files = await handler!.serialize(mock as any); + const xml = files[0].content; + + assert.ok(xml.includes('X')); + }); + }); + + describe('serialize — fallback', () => { + it('falls back to minimal DD02V when getSource fails', async () => { + const mock = createMockTable({ + name: 'ZFAIL_TABLE', + description: 'Fallback Table', + }); + mock.getSource = async () => { + throw new Error('Source not available'); + }; + + const files = await handler!.serialize(mock as any); + assert.strictEqual(files.length, 1); + const xml = files[0].content; + assert.ok(xml.includes('ZFAIL_TABLE')); + }); + + it('falls back when CDS source is unparseable', async () => { + const mock = createMockTable({ + name: 'ZBAD_TABLE', + description: 'Bad Source', + cdsSource: 'not valid CDS at all {{{{', + }); + + const files = await handler!.serialize(mock as any); + assert.strictEqual(files.length, 1); + const xml = files[0].content; + assert.ok(xml.includes('ZBAD_TABLE')); + }); + }); + + describe('fromAbapGit', () => { + it('maps DD02V to ADK data for transparent table', () => { + const result = handler!.fromAbapGit!({ + DD02V: { + TABNAME: 'ZTESTTABLE', + TABCLASS: 'TRANSP', + DDTEXT: 'Test Table', + DDLANGUAGE: 'E', + }, + }); + + assert.strictEqual(result.name, 'ZTESTTABLE'); + assert.strictEqual(result.type, 'TABL/DT'); + assert.strictEqual(result.description, 'Test Table'); + }); + + it('maps DD02V to ADK data for structure', () => { + const result = handler!.fromAbapGit!({ + DD02V: { + TABNAME: 'ZTESTSTRUCT', + TABCLASS: 'INTTAB', + DDTEXT: 'Test Structure', + DDLANGUAGE: 'D', + }, + }); + + assert.strictEqual(result.name, 'ZTESTSTRUCT'); + assert.strictEqual(result.type, 'TABL/DS'); + }); + }); +}); + +describe('CDS-to-abapGit mapping', () => { + it('buildDD02V maps structure annotations correctly', () => { + const { ast } = parse(CDS_STRUCTURE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'AGE Test Structure'); + + assert.strictEqual(dd02v.TABNAME, 'ZAGE_STRUCTURE'); + assert.strictEqual(dd02v.TABCLASS, 'INTTAB'); + assert.strictEqual(dd02v.EXCLASS, '1'); + assert.strictEqual(dd02v.DDLANGUAGE, 'E'); + assert.strictEqual(dd02v.MASTERLANG, 'E'); + }); + + it('buildDD02V maps transparent table annotations', () => { + const { ast } = parse(CDS_TRANSPARENT_TABLE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'AGE Test Transparent Table'); + + assert.strictEqual(dd02v.TABCLASS, 'TRANSP'); + assert.strictEqual(dd02v.CONTFLAG, 'A'); + assert.strictEqual(dd02v.EXCLASS, '1'); + }); + + it('buildDD03P maps builtin type fields', () => { + const { ast } = parse(CDS_STRUCTURE); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members); + + assert.strictEqual(entries.length, 3); + + // PARTNER_ID: abap.char(10) + assert.strictEqual(entries[0].FIELDNAME, 'PARTNER_ID'); + assert.strictEqual(entries[0].INTTYPE, 'C'); + assert.strictEqual(entries[0].DATATYPE, 'CHAR'); + assert.strictEqual(entries[0].LENG, '000010'); + assert.strictEqual(entries[0].POSITION, '0001'); + + // AMOUNT: abap.curr(15,2) + assert.strictEqual(entries[2].FIELDNAME, 'AMOUNT'); + assert.strictEqual(entries[2].INTTYPE, 'P'); + assert.strictEqual(entries[2].DATATYPE, 'CURR'); + assert.strictEqual(entries[2].LENG, '000015'); + assert.strictEqual(entries[2].DECIMALS, '000002'); + }); + + it('buildDD03P maps data element references', () => { + const { ast } = parse(CDS_TRANSPARENT_TABLE); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members); + + // MANDT — data element reference + const mandt = entries[0]; + assert.strictEqual(mandt.FIELDNAME, 'MANDT'); + assert.strictEqual(mandt.ROLLNAME, 'MANDT'); + assert.strictEqual(mandt.COMPTYPE, 'E'); + assert.strictEqual(mandt.KEYFLAG, 'X'); + assert.strictEqual(mandt.NOTNULL, 'X'); + }); + + it('buildDD03P maps key and not null flags', () => { + const { ast } = parse(CDS_TRANSPARENT_TABLE); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members); + + // key_field: key, not null, builtin + const keyField = entries[1]; + assert.strictEqual(keyField.KEYFLAG, 'X'); + assert.strictEqual(keyField.NOTNULL, 'X'); + + // value_field: not key, nullable + const valueField = entries[2]; + assert.strictEqual(keyField.FIELDNAME, 'KEY_FIELD'); + assert.strictEqual(valueField.KEYFLAG, undefined); + assert.strictEqual(valueField.NOTNULL, undefined); + }); +}); diff --git a/packages/adt-plugin-abapgit/tsconfig.lib.json b/packages/adt-plugin-abapgit/tsconfig.lib.json index 239616db..85d5499e 100644 --- a/packages/adt-plugin-abapgit/tsconfig.lib.json +++ b/packages/adt-plugin-abapgit/tsconfig.lib.json @@ -11,6 +11,9 @@ }, "include": ["src/**/*.ts"], "references": [ + { + "path": "../adt-schemas" + }, { "path": "../ts-xsd" }, diff --git a/packages/adt-plugin-abapgit/xsd/asx.xsd b/packages/adt-plugin-abapgit/xsd/asx.xsd index 2e1a8c4a..43993050 100644 --- a/packages/adt-plugin-abapgit/xsd/asx.xsd +++ b/packages/adt-plugin-abapgit/xsd/asx.xsd @@ -19,9 +19,12 @@ + + + - + diff --git a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd index fd33ba42..55c4e6a5 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd @@ -2,20 +2,22 @@ + + - + + - diff --git a/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd b/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd index 74dd5b3d..bd746ce7 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd @@ -2,6 +2,7 @@ + @@ -13,14 +14,14 @@ + - - + diff --git a/packages/adt-plugin-abapgit/xsd/types/dd04v.xsd b/packages/adt-plugin-abapgit/xsd/types/dd04v.xsd index 3b0328b3..48eed164 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd04v.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd04v.xsd @@ -3,26 +3,28 @@ - + - - - - + + + + + + - + diff --git a/packages/adt-plugin/src/cli-types.ts b/packages/adt-plugin/src/cli-types.ts index f7c95c44..714f3def 100644 --- a/packages/adt-plugin/src/cli-types.ts +++ b/packages/adt-plugin/src/cli-types.ts @@ -129,6 +129,12 @@ export interface CliCommandPlugin { */ name: string; + /** + * Command alias (alternate name) + * @example alias: 'deploy' allows `adt deploy` in addition to `adt export` + */ + alias?: string; + /** * Command description shown in help */ diff --git a/tsconfig.json b/tsconfig.json index 06cad868..5d5b5856 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -77,6 +77,15 @@ }, { "path": "./packages/adt-export" + }, + { + "path": "./packages/adt-aunit" + }, + { + "path": "./packages/adt-mcp" + }, + { + "path": "./packages/acds" } ] } From 66a6348884546248b1d7f67a866359d8efa734d9 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Tue, 17 Mar 2026 21:01:04 +0100 Subject: [PATCH 02/19] chore: update abapgit-examples submodule to c1980b0 --- git_modules/abapgit-examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git_modules/abapgit-examples b/git_modules/abapgit-examples index 3161cc18..c1980b06 160000 --- a/git_modules/abapgit-examples +++ b/git_modules/abapgit-examples @@ -1 +1 @@ -Subproject commit 3161cc18c1cea9004ddb1cf49a5faad4c3539427 +Subproject commit c1980b062cd4594bd9951b6ff52b00f7b501467b From 130168da09c858469f9fe420d662cb875499bd29 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 00:06:08 +0100 Subject: [PATCH 03/19] feat: add adt diff command and fix CDS-to-abapGit serialization New package @abapify/adt-diff with command that compares local abapGit XML files against remote SAP objects. Uses projection-based XML normalization to eliminate false positives from formatting and field ordering differences. Key fixes in CDS-to-abapGit serializer: - Fix REFFIELD: strip table prefix from annotation value (e.g. ZAGE_STRUCTURE.CURRENCY_CODE -> CURRENCY_CODE) - Fix CUKY/UNIT default lengths (cuky=5, unit=3) so INTLEN and LENG are correctly emitted - Fix include serialization: plain include -> .INCLUDE only, include with suffix -> .INCLU- only (was emitting both) - Fix projectOnto array projection to use union of all element keys instead of just the first element's keys CDS parser (acds) enhancements: - Support 'include with suffix ' syntax - Add Suffix keyword token, AST node, and visitor support Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .prettierignore | 3 +- adt.config.ts | 2 + git_modules/abapgit-examples | 2 +- packages/acds/src/ast.ts | 3 +- packages/acds/src/parser.ts | 6 + packages/acds/src/tokens.ts | 7 + packages/acds/src/visitor.ts | 5 +- packages/adt-cli/package.json | 1 + packages/adt-cli/src/bin/adt-all.ts | 3 +- packages/adt-diff/package.json | 37 + packages/adt-diff/project.json | 12 + packages/adt-diff/src/commands/diff.ts | 440 ++++++++++++ packages/adt-diff/src/index.ts | 28 + packages/adt-diff/src/lib/abapgit-to-cds.ts | 496 +++++++++++++ .../adt-diff/tests/abapgit-to-cds.test.ts | 668 ++++++++++++++++++ packages/adt-diff/tsconfig.json | 26 + packages/adt-diff/tsdown.config.ts | 10 + packages/adt-diff/vitest.config.ts | 7 + packages/adt-plugin-abapgit/src/index.ts | 11 + .../src/lib/deserializer.ts | 2 +- .../src/lib/handlers/cds-to-abapgit.ts | 208 ++++-- .../src/lib/handlers/objects/tabl.ts | 2 +- .../src/schemas/generated/schemas/tabl.ts | 5 + .../src/schemas/generated/types/tabl.ts | 3 + .../tests/handlers/tabl.test.ts | 303 ++++++++ .../adt-plugin-abapgit/xsd/types/dd02v.xsd | 1 + 26 files changed, 2235 insertions(+), 56 deletions(-) create mode 100644 packages/adt-diff/package.json create mode 100644 packages/adt-diff/project.json create mode 100644 packages/adt-diff/src/commands/diff.ts create mode 100644 packages/adt-diff/src/index.ts create mode 100644 packages/adt-diff/src/lib/abapgit-to-cds.ts create mode 100644 packages/adt-diff/tests/abapgit-to-cds.test.ts create mode 100644 packages/adt-diff/tsconfig.json create mode 100644 packages/adt-diff/tsdown.config.ts create mode 100644 packages/adt-diff/vitest.config.ts diff --git a/.prettierignore b/.prettierignore index e26f0b3f..89d58b42 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,5 @@ /dist /coverage /.nx/cache -/.nx/workspace-data \ No newline at end of file +/.nx/workspace-data +/git_modules \ No newline at end of file diff --git a/adt.config.ts b/adt.config.ts index f760b2c2..951c381d 100644 --- a/adt.config.ts +++ b/adt.config.ts @@ -23,5 +23,7 @@ export default { '@abapify/adt-export/commands/roundtrip', // Activate - bulk activate inactive objects '@abapify/adt-export/commands/activate', + // Diff - compare local abapGit files against SAP remote + '@abapify/adt-diff/commands/diff', ], } as AdtConfig; diff --git a/git_modules/abapgit-examples b/git_modules/abapgit-examples index c1980b06..aaa1d729 160000 --- a/git_modules/abapgit-examples +++ b/git_modules/abapgit-examples @@ -1 +1 @@ -Subproject commit c1980b062cd4594bd9951b6ff52b00f7b501467b +Subproject commit aaa1d7297a93e2dbafb4b39441bd355f27f61ae3 diff --git a/packages/acds/src/ast.ts b/packages/acds/src/ast.ts index f1f7535b..d37d0909 100644 --- a/packages/acds/src/ast.ts +++ b/packages/acds/src/ast.ts @@ -112,10 +112,11 @@ export interface FieldDefinition extends AstNode { notNull: boolean; } -/** Include directive: include ; */ +/** Include directive: include ; or include with suffix ; */ export interface IncludeDirective extends AstNode { kind: 'include'; name: string; + suffix?: string; } export type TableMember = FieldDefinition | IncludeDirective; diff --git a/packages/acds/src/parser.ts b/packages/acds/src/parser.ts index 7a9b6d8b..922df363 100644 --- a/packages/acds/src/parser.ts +++ b/packages/acds/src/parser.ts @@ -31,6 +31,7 @@ import { Null, As, Include, + Suffix, True, False, Abap, @@ -123,6 +124,11 @@ export class CdsParser extends CstParser { private includeDirective = this.RULE('includeDirective', () => { this.CONSUME(Include); this.SUBRULE(this.cdsName); + this.OPTION(() => { + this.CONSUME(With); + this.CONSUME(Suffix); + this.SUBRULE2(this.cdsName); + }); this.CONSUME(Semicolon); }); diff --git a/packages/acds/src/tokens.ts b/packages/acds/src/tokens.ts index 68f15dd2..b8cae9d8 100644 --- a/packages/acds/src/tokens.ts +++ b/packages/acds/src/tokens.ts @@ -145,6 +145,12 @@ export const Include = createToken({ longer_alt: Identifier, }); +export const Suffix = createToken({ + name: 'Suffix', + pattern: /suffix/, + longer_alt: Identifier, +}); + export const True = createToken({ name: 'True', pattern: /true/, @@ -210,6 +216,7 @@ export const allTokens = [ Null, As, Include, + Suffix, True, False, Abap, diff --git a/packages/acds/src/visitor.ts b/packages/acds/src/visitor.ts index f2baa119..ae538e50 100644 --- a/packages/acds/src/visitor.ts +++ b/packages/acds/src/visitor.ts @@ -102,7 +102,10 @@ export class CdsVisitor extends BaseCstVisitor { includeDirective(ctx: Record): IncludeDirective { const name = this.visit(ctx.cdsName[0]) as string; - return { kind: 'include', name }; + const suffix = ctx.cdsName?.[1] + ? (this.visit(ctx.cdsName[1]) as string) + : undefined; + return { kind: 'include', name, ...(suffix !== undefined && { suffix }) }; } fieldDefinition(ctx: Record): FieldDefinition { diff --git a/packages/adt-cli/package.json b/packages/adt-cli/package.json index 9a4de376..6a5a1948 100644 --- a/packages/adt-cli/package.json +++ b/packages/adt-cli/package.json @@ -29,6 +29,7 @@ "@abapify/adt-contracts": "^0.1.7", "@abapify/adt-codegen": "^0.1.7", "@abapify/adt-atc": "^0.1.7", + "@abapify/adt-diff": "workspace:*", "@abapify/adt-export": "^0.1.7", "chalk": "^5.6.2", "commander": "^12.0.0", diff --git a/packages/adt-cli/src/bin/adt-all.ts b/packages/adt-cli/src/bin/adt-all.ts index 6341be57..d7740e70 100644 --- a/packages/adt-cli/src/bin/adt-all.ts +++ b/packages/adt-cli/src/bin/adt-all.ts @@ -21,11 +21,12 @@ process.env.ADT_CLI_MODE = 'true'; import { codegenCommand } from '@abapify/adt-codegen/commands/codegen'; import { atcCommand } from '@abapify/adt-atc/commands/atc'; import { exportCommand } from '@abapify/adt-export/commands/export'; +import { diffCommand } from '@abapify/adt-diff/commands/diff'; import { main } from '../lib/cli'; main({ - preloadedPlugins: [codegenCommand, atcCommand, exportCommand], + preloadedPlugins: [codegenCommand, atcCommand, exportCommand, diffCommand], }).catch((error) => { console.error('❌ CLI Error:', error.message); process.exit(1); diff --git a/packages/adt-diff/package.json b/packages/adt-diff/package.json new file mode 100644 index 00000000..866d0063 --- /dev/null +++ b/packages/adt-diff/package.json @@ -0,0 +1,37 @@ +{ + "name": "@abapify/adt-diff", + "publishConfig": { + "access": "public" + }, + "version": "0.1.0", + "description": "Diff CLI plugin for adt-cli - compare local serialized files against SAP remote source", + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": "./dist/index.mjs", + "./commands/diff": "./dist/commands/diff.mjs", + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "sap", + "adt", + "abap", + "diff", + "compare", + "abapgit" + ], + "dependencies": { + "@abapify/adk": "workspace:*", + "@abapify/adt-plugin": "workspace:*", + "@abapify/adt-plugin-abapgit": "workspace:*", + "chalk": "^5.6.2", + "diff": "^8.0.3", + "fast-xml-parser": "^5.5.5" + }, + "module": "./dist/index.mjs" +} diff --git a/packages/adt-diff/project.json b/packages/adt-diff/project.json new file mode 100644 index 00000000..8fe92391 --- /dev/null +++ b/packages/adt-diff/project.json @@ -0,0 +1,12 @@ +{ + "name": "adt-diff", + "targets": { + "test": { + "command": "vitest run", + "options": { + "cwd": "{projectRoot}" + }, + "inputs": ["{projectRoot}/tests/**/*.ts", "{projectRoot}/src/**/*.ts"] + } + } +} diff --git a/packages/adt-diff/src/commands/diff.ts b/packages/adt-diff/src/commands/diff.ts new file mode 100644 index 00000000..1f0a4674 --- /dev/null +++ b/packages/adt-diff/src/commands/diff.ts @@ -0,0 +1,440 @@ +/** + * Diff Command Plugin — Type-Agnostic + * + * Compares local abapGit files against what SAP has. + * + * The local file (from abapGit) is the source of truth — it defines which + * fields matter. The remote side is fetched from SAP via ADT, serialized + * through the same handler, then **projected** onto the local's field set. + * Any extra fields the serializer adds (LANGDEP, POSITION, etc.) are + * stripped so the diff only shows real value changes. + * + * For XML metadata: parse both → project remote onto local's keys → rebuild + * For .abap source: compare text directly + * + * Works for ALL object types supported by adt-plugin-abapgit: + * CLAS, INTF, PROG, FUGR, TABL, DOMA, DTEL, TTYP, DEVC + * + * Usage: + * adt diff zage_structure.tabl.xml + * adt diff zcl_myclass.clas.xml + * adt diff zif_myintf.intf.xml --no-color + */ + +import type { CliCommandPlugin, CliContext } from '@abapify/adt-plugin'; +import { createAdk, type AdtClient } from '@abapify/adk'; +import { readFileSync, existsSync, readdirSync } from 'node:fs'; +import { resolve, basename, dirname, join } from 'node:path'; +import { createTwoFilesPatch } from 'diff'; +import chalk from 'chalk'; +import { + getHandler, + getSupportedTypes, + parseAbapGitFilename, + type ObjectHandler, +} from '@abapify/adt-plugin-abapgit'; + +/** + * Collect all local files belonging to one abapGit object. + * + * Given the XML metadata file, scans the same directory for companion + * .abap files that share the same name.type prefix. + * + * @returns Map of relative filename → content + */ +function collectLocalFiles( + xmlPath: string, + objectName: string, + fileExtension: string, +): Map { + const dir = dirname(xmlPath); + const prefix = `${objectName}.${fileExtension}`; + const files = new Map(); + + for (const entry of readdirSync(dir)) { + if (entry.toLowerCase().startsWith(prefix)) { + const fullPath = join(dir, entry); + files.set(entry.toLowerCase(), readFileSync(fullPath, 'utf-8')); + } + } + + return files; +} + +/** + * Project `source` onto `reference`'s key structure. + * + * Recursively keeps only the keys from `source` that also exist in + * `reference`. For arrays, builds a **union** of all keys across all + * reference elements as the template — because different elements may + * have different optional fields (e.g. DD03P entries where some have + * DECIMALS and others don't). + * + * This ensures the remote object only contains fields the local cares + * about, so serializer-added extras (LANGDEP, POSITION, etc.) vanish. + */ +function projectOnto(source: unknown, reference: unknown): unknown { + if (reference === null || reference === undefined) return source; + if (source === null || source === undefined) return source; + + // Both arrays — build union template from ALL reference elements + if (Array.isArray(reference) && Array.isArray(source)) { + if (reference.length === 0) return source; + + // Merge all reference elements into a union shape + const unionShape = mergeObjectKeys(reference); + if (unionShape === undefined) return source; + + return source.map((item) => projectOnto(item, unionShape)); + } + + // Both objects — keep only keys present in reference + if ( + typeof reference === 'object' && + typeof source === 'object' && + !Array.isArray(reference) && + !Array.isArray(source) + ) { + const ref = reference as Record; + const src = source as Record; + const result: Record = {}; + for (const key of Object.keys(ref)) { + if (key in src) { + result[key] = projectOnto(src[key], ref[key]); + } + } + return result; + } + + // Primitives — return source as-is + return source; +} + +/** + * Merge all keys from an array of objects into a single union object. + * Each key gets the first non-undefined value found across elements. + * Used to build the "widest" template for array element projection. + */ +function mergeObjectKeys( + items: unknown[], +): Record | undefined { + const objects = items.filter( + (item): item is Record => + item !== null && typeof item === 'object' && !Array.isArray(item), + ); + if (objects.length === 0) return undefined; + + const union: Record = {}; + for (const obj of objects) { + for (const [key, value] of Object.entries(obj)) { + if (!(key in union) || union[key] === undefined) { + union[key] = value; + } + } + } + return union; +} + +/** + * Normalize an XML pair for comparison. + * + * 1. Parse both local and remote through the schema (strips formatting) + * 2. Project remote values onto local's field structure (strips extras) + * 3. Rebuild both through the same builder (identical formatting) + * + * Returns [normalizedLocal, normalizedRemote]. + */ +function normalizeXmlPair( + localXml: string, + remoteXml: string, + handler: ObjectHandler, +): [string, string] { + try { + const localParsed = handler.schema.parse(localXml); + const remoteParsed = handler.schema.parse(remoteXml); + + // Project remote onto local's shape — drop fields local doesn't have + const remoteProjected = projectOnto(remoteParsed, localParsed); + + const normalizedLocal = handler.schema.build(localParsed, { pretty: true }); + const normalizedRemote = handler.schema.build( + remoteProjected as typeof localParsed, + { pretty: true }, + ); + + return [normalizedLocal, normalizedRemote]; + } catch { + // If parsing fails, return as-is + return [localXml, remoteXml]; + } +} + +/** + * Print a unified diff with optional color. + * Returns true if differences were found. + */ +function printDiff( + localLabel: string, + remoteLabel: string, + localContent: string, + remoteContent: string, + contextLines: number, + useColor: boolean, +): boolean { + const local = localContent.endsWith('\n') + ? localContent + : localContent + '\n'; + const remote = remoteContent.endsWith('\n') + ? remoteContent + : remoteContent + '\n'; + + if (local === remote) return false; + + const patch = createTwoFilesPatch( + `a/${localLabel}`, + `b/${remoteLabel}`, + local, + remote, + 'local', + 'remote (SAP)', + { context: contextLines }, + ); + + for (const line of patch.split('\n')) { + if (!useColor) { + console.log(line); + continue; + } + if (line.startsWith('+++') || line.startsWith('---')) { + console.log(chalk.bold(line)); + } else if (line.startsWith('+')) { + console.log(chalk.green(line)); + } else if (line.startsWith('-')) { + console.log(chalk.red(line)); + } else if (line.startsWith('@@')) { + console.log(chalk.cyan(line)); + } else { + console.log(line); + } + } + + return true; +} + +export const diffCommand: CliCommandPlugin = { + name: 'diff', + description: + 'Compare local abapGit files against SAP remote (any supported object type)', + + arguments: [ + { + name: '', + description: `Local .xml file to compare (e.g., zcl_myclass.clas.xml). Supported types: ${getSupportedTypes().join(', ')}`, + }, + ], + + options: [ + { + flags: '--no-color', + description: 'Disable colored output', + }, + { + flags: '-c, --context ', + description: 'Number of context lines in diff', + default: '3', + }, + ], + + async execute(args: Record, ctx: CliContext) { + const filePath = args.file as string; + const contextLines = parseInt(String(args.context ?? '3'), 10); + const useColor = args.color !== false; + + // Resolve file path + const fullPath = resolve(ctx.cwd, filePath); + if (!existsSync(fullPath)) { + ctx.logger.error(`File not found: ${fullPath}`); + process.exit(1); + } + + // Parse filename to detect type + const filename = basename(fullPath); + const parsed = parseAbapGitFilename(filename); + if (!parsed) { + ctx.logger.error( + `Cannot parse filename: ${filename}. Expected abapGit format: name.type.xml`, + ); + process.exit(1); + } + + if (parsed.extension !== 'xml') { + ctx.logger.error( + `Expected .xml metadata file, got .${parsed.extension}. Pass the .xml file, not .abap.`, + ); + process.exit(1); + } + + // Look up handler from abapGit registry + const handler = getHandler(parsed.type); + if (!handler) { + ctx.logger.error( + `Unsupported object type: ${parsed.type}. Supported: ${getSupportedTypes().join(', ')}`, + ); + process.exit(1); + } + + // Collect local files for this object + const objectName = parsed.name.toLowerCase(); + const localFiles = collectLocalFiles( + fullPath, + objectName, + handler.fileExtension, + ); + + console.log( + `\n${useColor ? chalk.bold('Diff:') : 'Diff:'} ${parsed.name} (${parsed.type}) — ${localFiles.size} file(s)`, + ); + + // Need ADT client for remote comparison + if (!ctx.getAdtClient) { + ctx.logger.error('ADT client not available. Run: adt auth login'); + process.exit(1); + } + + // Parse local XML to extract ADK type info via fromAbapGit + const localXml = readFileSync(fullPath, 'utf-8'); + let adkType = parsed.type; + + if (handler.fromAbapGit) { + try { + const parsedXml = handler.schema.parse(localXml); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const values = (parsedXml as any)?.abapGit?.abap?.values ?? {}; + const payload = handler.fromAbapGit(values); + if (typeof payload.type === 'string') { + adkType = payload.type; + } + } catch { + // Fall through — use filename-derived type + } + } + + // Fetch remote ADK object + console.log( + `${useColor ? chalk.dim('Fetching') : 'Fetching'} ${parsed.name} (${adkType}) from SAP...`, + ); + + const client = await ctx.getAdtClient!(); + const adk = createAdk(client as AdtClient); + const remoteObj = adk.get(parsed.name, adkType); + + try { + await remoteObj.load(); + } catch (error) { + ctx.logger.error( + `Failed to load remote object: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } + + // Serialize remote using the same handler → produces SerializedFile[] + let remoteFiles; + try { + remoteFiles = await handler.serialize(remoteObj); + } catch (error) { + ctx.logger.error( + `Failed to serialize remote object: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } + + // Build remote file map (lowercase path → content) + const remoteMap = new Map(); + for (const f of remoteFiles) { + remoteMap.set(f.path.toLowerCase(), f.content); + } + + // Diff each file + let hasDifferences = false; + let identicalCount = 0; + + for (const [localPath, localContent] of localFiles) { + const remoteContent = remoteMap.get(localPath); + if (remoteContent === undefined) { + console.log( + useColor + ? chalk.yellow(`\n + Only in local: ${localPath}`) + : `\n + Only in local: ${localPath}`, + ); + hasDifferences = true; + continue; + } + + // For XML files: normalize both through schema, projecting remote + // onto local's field set to strip serializer-added extras + const isXml = localPath.endsWith('.xml'); + let diffLocal = localContent; + let diffRemote = remoteContent; + if (isXml) { + [diffLocal, diffRemote] = normalizeXmlPair( + localContent, + remoteContent, + handler, + ); + } + + const diffFound = printDiff( + localPath, + localPath, + diffLocal, + diffRemote, + contextLines, + useColor, + ); + if (diffFound) { + hasDifferences = true; + } else { + identicalCount++; + } + } + + // Files present in remote but not locally + for (const [remotePath] of remoteMap) { + if (!localFiles.has(remotePath)) { + console.log( + useColor + ? chalk.yellow(`\n - Only in remote: ${remotePath}`) + : `\n - Only in remote: ${remotePath}`, + ); + hasDifferences = true; + } + } + + // Summary + console.log(''); + if (!hasDifferences) { + console.log( + useColor + ? chalk.green( + `No differences found. (${identicalCount} file(s) identical)`, + ) + : `No differences found. (${identicalCount} file(s) identical)`, + ); + return; + } + + console.log( + useColor + ? chalk.red('Differences found.') + + (identicalCount > 0 + ? chalk.dim(` (${identicalCount} file(s) identical)`) + : '') + : `Differences found.${identicalCount > 0 ? ` (${identicalCount} file(s) identical)` : ''}`, + ); + + // Exit with non-zero if differences exist (standard diff convention) + process.exit(1); + }, +}; + +export default diffCommand; diff --git a/packages/adt-diff/src/index.ts b/packages/adt-diff/src/index.ts new file mode 100644 index 00000000..a560cf94 --- /dev/null +++ b/packages/adt-diff/src/index.ts @@ -0,0 +1,28 @@ +/** + * @abapify/adt-diff + * + * Diff CLI plugin for adt-cli — compare local abapGit files + * against SAP remote source. Works with any object type supported + * by @abapify/adt-plugin-abapgit. + * + * @example + * ```typescript + * // In adt.config.ts + * export default { + * commands: [ + * '@abapify/adt-diff/commands/diff', + * ], + * }; + * ``` + */ + +export { diffCommand } from './commands/diff'; + +// TABL-specific CDS DDL builder (optional utility — not used by diff command) +export { + buildCdsDdl, + tablXmlToCdsDdl, + parseTablXml, + type DD02VData, + type DD03PData, +} from './lib/abapgit-to-cds'; diff --git a/packages/adt-diff/src/lib/abapgit-to-cds.ts b/packages/adt-diff/src/lib/abapgit-to-cds.ts new file mode 100644 index 00000000..9174e71d --- /dev/null +++ b/packages/adt-diff/src/lib/abapgit-to-cds.ts @@ -0,0 +1,496 @@ +/** + * abapGit DD02V/DD03P → CDS DDL Source Builder + * + * Reverse mapping of cds-to-abapgit.ts — reconstructs CDS DDL source code + * from parsed abapGit XML data (DD02V header + DD03P field entries). + * + * Used by `adt diff` to compare local abapGit serialized files against + * SAP remote CDS source fetched via ADT /source/main endpoint. + */ + +// ============================================ +// DD02V / DD03P input types +// (mirror of cds-to-abapgit.ts output types) +// ============================================ + +export interface DD02VData { + TABNAME?: string; + DDLANGUAGE?: string; + TABCLASS?: string; + LANGDEP?: string; + DDTEXT?: string; + MASTERLANG?: string; + CONTFLAG?: string; + EXCLASS?: string; + AUTHCLASS?: string; + CLIDEP?: string; + BUFFERED?: string; + MATEFLAG?: string; + SHLPEXI?: string; +} + +export interface DD03PData { + FIELDNAME?: string; + POSITION?: string; + KEYFLAG?: string; + ROLLNAME?: string; + ADMINFIELD?: string; + INTTYPE?: string; + INTLEN?: string; + NOTNULL?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + SHLPORIGIN?: string; + MASK?: string; + COMPTYPE?: string; + REFTABLE?: string; + REFFIELD?: string; + PRECFIELD?: string; + DDTEXT?: string; + DOMNAME?: string; +} + +// ============================================ +// Reverse mapping tables +// ============================================ + +/** DDIC DATATYPE → CDS abap. name */ +const DATATYPE_TO_CDS: Record = { + CHAR: 'char', + CLNT: 'clnt', + NUMC: 'numc', + DATS: 'dats', + DATN: 'datn', + TIMS: 'tims', + TIMN: 'timn', + DEC: 'dec', + CURR: 'curr', + QUAN: 'quan', + RAW: 'raw', + INT1: 'int1', + INT2: 'int2', + INT4: 'int4', + INT8: 'int8', + FLTP: 'fltp', + STRG: 'string', + RSTR: 'rawstring', + CUKY: 'cuky', + UNIT: 'unit', + LANG: 'lang', + ACCP: 'accp', + PREC: 'prec', + D16N: 'd16n', + D34N: 'd34n', + D16R: 'd16r', + D34R: 'd34r', + D16D: 'd16d', + D34D: 'd34d', + UTCL: 'utclong', + SSTR: 'sstring', + LCHR: 'lchr', + LRAW: 'lraw', +}; + +/** Types that never take a length parameter in CDS */ +const NO_LENGTH_TYPES = new Set([ + 'clnt', + 'dats', + 'datn', + 'tims', + 'timn', + 'int1', + 'int2', + 'int4', + 'int8', + 'fltp', + 'lang', + 'accp', + 'prec', + 'd16n', + 'd34n', + 'd16r', + 'd34r', + 'utclong', +]); + +/** Types that take decimals in CDS: abap.dec(len,dec) */ +const DECIMAL_TYPES = new Set(['dec', 'curr', 'quan', 'fltp', 'd16d', 'd34d']); + +/** Types that are variable-length (string, rawstring) — length 0 is omitted */ +const VARIABLE_LENGTH_TYPES = new Set(['string', 'rawstring']); + +/** DD02V EXCLASS → CDS @AbapCatalog.enhancement.category */ +const ENHANCEMENT_CATEGORY_REVERSE: Record = { + '0': '#NOT_CLASSIFIED', + '1': '#NOT_EXTENSIBLE', + '2': '#EXTENSIBLE_CHARACTER_NUMERIC', + '3': '#EXTENSIBLE_CHARACTER', + '4': '#EXTENSIBLE_ANY', +}; + +/** DD02V CONTFLAG → CDS @AbapCatalog.deliveryClass */ +const DELIVERY_CLASS_REVERSE: Record = { + A: '#A', + C: '#C', + L: '#L', + G: '#G', + E: '#E', + S: '#S', + W: '#W', +}; + +/** DD02V TABCLASS → CDS @AbapCatalog.tableCategory */ +const TABLE_CATEGORY_REVERSE: Record = { + TRANSP: '#TRANSPARENT', + CLUSTER: '#CLUSTER', + POOL: '#POOL', + APPEND: '#APPEND', +}; + +/** DD02V MATEFLAG → CDS @AbapCatalog.dataMaintenance */ +const DATA_MAINTENANCE_REVERSE: Record = { + X: '#RESTRICTED', + N: '#NOT_ALLOWED', + '': '#ALLOWED', +}; + +// ============================================ +// Builder +// ============================================ + +/** + * Build CDS DDL source from DD02V header and DD03P field entries. + * + * @param dd02v - Table/structure header data + * @param dd03pEntries - Field entries (sorted by POSITION if present) + * @returns CDS DDL source string + */ +export function buildCdsDdl( + dd02v: DD02VData, + dd03pEntries: DD03PData[], +): string { + const lines: string[] = []; + const tableName = dd02v.TABNAME ?? ''; + const isStructure = dd02v.TABCLASS === 'INTTAB'; + + // --- Annotations --- + if (dd02v.DDTEXT) { + lines.push( + `@EndUserText.label : '${escapeAnnotationString(dd02v.DDTEXT)}'`, + ); + } + + if (dd02v.EXCLASS) { + const cat = ENHANCEMENT_CATEGORY_REVERSE[dd02v.EXCLASS]; + if (cat) { + lines.push(`@AbapCatalog.enhancement.category : ${cat}`); + } + } + + if (!isStructure) { + // Table-specific annotations + if (dd02v.TABCLASS) { + const cat = TABLE_CATEGORY_REVERSE[dd02v.TABCLASS]; + if (cat) { + lines.push(`@AbapCatalog.tableCategory : ${cat}`); + } + } + + if (dd02v.CONTFLAG) { + const dc = DELIVERY_CLASS_REVERSE[dd02v.CONTFLAG]; + if (dc) { + lines.push(`@AbapCatalog.deliveryClass : ${dc}`); + } + } + + if (dd02v.MATEFLAG !== undefined) { + const dm = DATA_MAINTENANCE_REVERSE[dd02v.MATEFLAG ?? '']; + if (dm) { + lines.push(`@AbapCatalog.dataMaintenance : ${dm}`); + } + } + } + + // --- Definition header --- + const keyword = isStructure ? 'define structure' : 'define table'; + lines.push(`${keyword} ${tableName.toLowerCase()} {`); + + // --- Sort fields by POSITION --- + const sorted = [...dd03pEntries].sort((a, b) => { + const posA = parseInt(a.POSITION ?? '0', 10); + const posB = parseInt(b.POSITION ?? '0', 10); + return posA - posB; + }); + + // --- Compute alignment --- + // Find max field name width for alignment (only field entries, not includes) + const fieldInfos = buildFieldInfos(sorted, tableName); + const maxNameWidth = Math.max( + ...fieldInfos + .filter((f): f is FieldInfo => f.kind === 'field') + .map((f) => f.prefix.length), + 0, + ); + + // --- Emit fields --- + for (const info of fieldInfos) { + if (info.kind === 'include') { + lines.push(` include ${info.name};`); + } else { + // Field annotations (e.g., @Semantics.amount.currencyCode) + for (const ann of info.annotations) { + lines.push(` ${ann}`); + } + + const paddedPrefix = info.prefix.padEnd(maxNameWidth); + lines.push(` ${paddedPrefix} : ${info.typeStr};`); + } + } + + lines.push('}'); + + return lines.join('\n'); +} + +// ============================================ +// Internal helpers +// ============================================ + +interface FieldInfo { + kind: 'field'; + /** "key fieldname" or "fieldname" — used for alignment */ + prefix: string; + /** CDS type string (e.g., "abap.char(10) not null") */ + typeStr: string; + /** Field-level annotations */ + annotations: string[]; +} + +interface IncludeInfo { + kind: 'include'; + name: string; +} + +type MemberInfo = FieldInfo | IncludeInfo; + +/** + * Pre-process DD03P entries into structured field info for emission. + */ +function buildFieldInfos( + entries: DD03PData[], + tableName: string, +): MemberInfo[] { + const result: MemberInfo[] = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const fieldName = entry.FIELDNAME ?? ''; + + // Skip .INCLU-_XX entries (part of include pair, handled with .INCLUDE) + if (fieldName === '.INCLU-_XX' || fieldName.startsWith('.INCLU-')) { + continue; + } + + // Include directive + if (fieldName === '.INCLUDE') { + const includeName = entry.PRECFIELD ?? ''; + if (includeName) { + result.push({ + kind: 'include', + name: includeName.toLowerCase(), + }); + } + continue; + } + + // Structure-typed field (COMPTYPE = 'S', not an include) + if (entry.COMPTYPE === 'S' && entry.DATATYPE === 'STRU') { + const isKey = entry.KEYFLAG === 'X'; + const notNull = entry.NOTNULL === 'X' || isKey; + const rollname = entry.ROLLNAME ?? fieldName; + + const prefix = isKey + ? `key ${fieldName.toLowerCase()}` + : fieldName.toLowerCase(); + + const typeStr = notNull + ? `${rollname.toLowerCase()} not null` + : rollname.toLowerCase(); + + result.push({ + kind: 'field', + prefix, + typeStr, + annotations: [], + }); + continue; + } + + // Data element reference (COMPTYPE = 'E') + if (entry.COMPTYPE === 'E' && entry.ROLLNAME) { + const isKey = entry.KEYFLAG === 'X'; + const notNull = entry.NOTNULL === 'X' || isKey; + + const prefix = isKey + ? `key ${fieldName.toLowerCase()}` + : fieldName.toLowerCase(); + + const typeStr = notNull + ? `${entry.ROLLNAME.toLowerCase()} not null` + : entry.ROLLNAME.toLowerCase(); + + result.push({ + kind: 'field', + prefix, + typeStr, + annotations: [], + }); + continue; + } + + // Built-in type field + const cdsType = buildCdsTypeString(entry); + if (!cdsType) continue; + + const isKey = entry.KEYFLAG === 'X'; + const notNull = entry.NOTNULL === 'X' || isKey; + const annotations: string[] = []; + + // @Semantics annotations for CURR/QUAN fields + if (entry.DATATYPE === 'CURR' && entry.REFTABLE && entry.REFFIELD) { + const refTable = + entry.REFTABLE.toUpperCase() === tableName.toUpperCase() + ? tableName.toLowerCase() + : entry.REFTABLE.toLowerCase(); + annotations.push( + `@Semantics.amount.currencyCode : '${refTable}.${entry.REFFIELD.toLowerCase()}'`, + ); + } else if (entry.DATATYPE === 'QUAN' && entry.REFTABLE && entry.REFFIELD) { + const refTable = + entry.REFTABLE.toUpperCase() === tableName.toUpperCase() + ? tableName.toLowerCase() + : entry.REFTABLE.toLowerCase(); + annotations.push( + `@Semantics.quantity.unitOfMeasure : '${refTable}.${entry.REFFIELD.toLowerCase()}'`, + ); + } + + const prefix = isKey + ? `key ${fieldName.toLowerCase()}` + : fieldName.toLowerCase(); + + const typeStr = notNull ? `${cdsType} not null` : cdsType; + + result.push({ + kind: 'field', + prefix, + typeStr, + annotations, + }); + } + + return result; +} + +/** + * Build the CDS type string from a DD03P entry. + * E.g., "abap.char(10)", "abap.dec(15,2)", "abap.int4" + */ +function buildCdsTypeString(entry: DD03PData): string | null { + const datatype = entry.DATATYPE; + if (!datatype) return null; + + const cdsName = DATATYPE_TO_CDS[datatype]; + if (!cdsName) return null; + + // Fixed-length types without parameters + if (NO_LENGTH_TYPES.has(cdsName)) { + return `abap.${cdsName}`; + } + + // Parse LENG and DECIMALS (strip leading zeros) + const leng = entry.LENG ? parseInt(entry.LENG, 10) : undefined; + const decimals = entry.DECIMALS ? parseInt(entry.DECIMALS, 10) : undefined; + + // Variable-length types (string, rawstring) + if (VARIABLE_LENGTH_TYPES.has(cdsName)) { + if (leng && leng > 0) { + return `abap.${cdsName}(${leng})`; + } + return `abap.${cdsName}(0)`; + } + + // Types with decimals + if (DECIMAL_TYPES.has(cdsName) && leng !== undefined) { + const dec = decimals ?? 0; + return `abap.${cdsName}(${leng},${dec})`; + } + + // Types with length only + if (leng !== undefined) { + return `abap.${cdsName}(${leng})`; + } + + // Fallback: no parameters + return `abap.${cdsName}`; +} + +/** + * Escape single quotes in annotation string values + */ +function escapeAnnotationString(value: string): string { + return value.replace(/'/g, "''"); +} + +// ============================================ +// XML parsing +// ============================================ + +import { XMLParser } from 'fast-xml-parser'; + +const xmlParser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + trimValues: true, + parseTagValue: false, + parseAttributeValue: false, + removeNSPrefix: true, + isArray: (name) => name === 'DD03P', +}); + +/** + * Parse abapGit TABL XML content and extract DD02V/DD03P data. + */ +export function parseTablXml(xmlContent: string): { + dd02v: DD02VData; + dd03p: DD03PData[]; +} { + const parsed = xmlParser.parse(xmlContent); + const values = parsed?.abapGit?.abap?.values ?? {}; + + const dd02v: DD02VData = values.DD02V ?? {}; + + // DD03P can be inside DD03P_TABLE or directly as array + let dd03pRaw = values.DD03P_TABLE?.DD03P ?? []; + if (!Array.isArray(dd03pRaw)) { + dd03pRaw = [dd03pRaw]; + } + + return { dd02v, dd03p: dd03pRaw }; +} + +// ============================================ +// Convenience: parse XML and build DDL +// ============================================ + +/** + * Parse abapGit TABL XML and build CDS DDL source. + * + * @param xmlContent - Raw XML content of a .tabl.xml file + * @returns CDS DDL source string + */ +export function tablXmlToCdsDdl(xmlContent: string): string { + const { dd02v, dd03p } = parseTablXml(xmlContent); + return buildCdsDdl(dd02v, dd03p); +} diff --git a/packages/adt-diff/tests/abapgit-to-cds.test.ts b/packages/adt-diff/tests/abapgit-to-cds.test.ts new file mode 100644 index 00000000..d88ae453 --- /dev/null +++ b/packages/adt-diff/tests/abapgit-to-cds.test.ts @@ -0,0 +1,668 @@ +/** + * Tests for abapgit-to-cds: DD02V/DD03P → CDS DDL builder + * + * Uses ground truth fixtures from git_modules/abapgit-examples/src/ddic/ + * to verify the reverse mapping produces valid CDS DDL. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + buildCdsDdl, + tablXmlToCdsDdl, + parseTablXml, + type DD02VData, + type DD03PData, +} from '../src/lib/abapgit-to-cds'; + +// ============================================ +// Fixture helpers +// ============================================ + +const FIXTURES_DIR = join( + __dirname, + '../../..', + 'git_modules/abapgit-examples/src/ddic', +); + +function loadFixture(filename: string): string { + return readFileSync(join(FIXTURES_DIR, filename), 'utf-8'); +} + +// ============================================ +// Unit tests: buildCdsDdl +// ============================================ + +describe('buildCdsDdl', () => { + describe('structure (INTTAB)', () => { + it('should generate define structure for TABCLASS=INTTAB', () => { + const dd02v: DD02VData = { + TABNAME: 'ZTEST_STRUCT', + TABCLASS: 'INTTAB', + DDTEXT: 'Test structure', + EXCLASS: '4', + }; + + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'FIELD1', + POSITION: '0001', + ADMINFIELD: '0', + INTTYPE: 'C', + INTLEN: '000020', + DATATYPE: 'CHAR', + LENG: '000010', + MASK: ' CHAR', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('define structure ztest_struct'); + expect(result).toContain("@EndUserText.label : 'Test structure'"); + expect(result).toContain( + '@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY', + ); + expect(result).not.toContain('@AbapCatalog.tableCategory'); + expect(result).toContain('field1 : abap.char(10)'); + }); + }); + + describe('transparent table (TRANSP)', () => { + it('should generate define table with table annotations', () => { + const dd02v: DD02VData = { + TABNAME: 'ZTEST_TABLE', + TABCLASS: 'TRANSP', + DDTEXT: 'Test table', + CONTFLAG: 'A', + EXCLASS: '1', + }; + + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'MANDT', + POSITION: '0001', + KEYFLAG: 'X', + ROLLNAME: 'MANDT', + ADMINFIELD: '0', + NOTNULL: 'X', + COMPTYPE: 'E', + }, + { + FIELDNAME: 'KEY_FIELD', + POSITION: '0002', + KEYFLAG: 'X', + ADMINFIELD: '0', + INTTYPE: 'C', + INTLEN: '000020', + DATATYPE: 'CHAR', + LENG: '000010', + NOTNULL: 'X', + MASK: ' CHAR', + }, + { + FIELDNAME: 'VALUE_FIELD', + POSITION: '0003', + ADMINFIELD: '0', + INTTYPE: 'C', + INTLEN: '000080', + DATATYPE: 'CHAR', + LENG: '000040', + MASK: ' CHAR', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('define table ztest_table'); + expect(result).toContain('@AbapCatalog.tableCategory : #TRANSPARENT'); + expect(result).toContain('@AbapCatalog.deliveryClass : #A'); + expect(result).toContain( + '@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE', + ); + expect(result).toMatch(/key mandt\s+: mandt not null/); + expect(result).toMatch(/key key_field\s+: abap\.char\(10\) not null/); + expect(result).toMatch(/value_field\s+: abap\.char\(40\)/); + }); + }); + + describe('builtin types', () => { + it('should map all fixed-length types without parameters', () => { + const dd02v: DD02VData = { + TABNAME: 'ZTEST', + TABCLASS: 'INTTAB', + }; + + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'F_INT1', + POSITION: '0001', + DATATYPE: 'INT1', + LENG: '000003', + INTTYPE: 'X', + }, + { + FIELDNAME: 'F_INT2', + POSITION: '0002', + DATATYPE: 'INT2', + LENG: '000005', + INTTYPE: 'X', + }, + { + FIELDNAME: 'F_INT4', + POSITION: '0003', + DATATYPE: 'INT4', + LENG: '000010', + INTTYPE: 'X', + }, + { + FIELDNAME: 'F_INT8', + POSITION: '0004', + DATATYPE: 'INT8', + LENG: '000019', + INTTYPE: '8', + }, + { + FIELDNAME: 'F_DATS', + POSITION: '0005', + DATATYPE: 'DATS', + LENG: '000008', + INTTYPE: 'D', + }, + { + FIELDNAME: 'F_TIMS', + POSITION: '0006', + DATATYPE: 'TIMS', + LENG: '000006', + INTTYPE: 'T', + }, + { + FIELDNAME: 'F_FLTP', + POSITION: '0007', + DATATYPE: 'FLTP', + LENG: '000016', + DECIMALS: '000016', + INTTYPE: 'F', + }, + { + FIELDNAME: 'F_CLNT', + POSITION: '0008', + DATATYPE: 'CLNT', + LENG: '000003', + INTTYPE: 'C', + }, + { + FIELDNAME: 'F_LANG', + POSITION: '0009', + DATATYPE: 'LANG', + LENG: '000001', + INTTYPE: 'C', + }, + { + FIELDNAME: 'F_UTCL', + POSITION: '0010', + DATATYPE: 'UTCL', + LENG: '000027', + INTTYPE: 'p', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('f_int1 : abap.int1'); + expect(result).toContain('f_int2 : abap.int2'); + expect(result).toContain('f_int4 : abap.int4'); + expect(result).toContain('f_int8 : abap.int8'); + expect(result).toContain('f_dats : abap.dats'); + expect(result).toContain('f_tims : abap.tims'); + expect(result).toContain('f_fltp : abap.fltp'); + expect(result).toContain('f_clnt : abap.clnt'); + expect(result).toContain('f_lang : abap.lang'); + expect(result).toContain('f_utcl : abap.utclong'); + }); + + it('should map types with length', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'F_CHAR', + POSITION: '0001', + DATATYPE: 'CHAR', + LENG: '000010', + }, + { + FIELDNAME: 'F_NUMC', + POSITION: '0002', + DATATYPE: 'NUMC', + LENG: '000005', + }, + { + FIELDNAME: 'F_RAW', + POSITION: '0003', + DATATYPE: 'RAW', + LENG: '000016', + }, + { + FIELDNAME: 'F_CUKY', + POSITION: '0004', + DATATYPE: 'CUKY', + LENG: '000005', + }, + { + FIELDNAME: 'F_UNIT', + POSITION: '0005', + DATATYPE: 'UNIT', + LENG: '000002', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('f_char : abap.char(10)'); + expect(result).toContain('f_numc : abap.numc(5)'); + expect(result).toContain('f_raw : abap.raw(16)'); + expect(result).toContain('f_cuky : abap.cuky(5)'); + expect(result).toContain('f_unit : abap.unit(2)'); + }); + + it('should map decimal types with length and decimals', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'F_DEC', + POSITION: '0001', + DATATYPE: 'DEC', + LENG: '000015', + DECIMALS: '000002', + }, + { + FIELDNAME: 'F_CURR', + POSITION: '0002', + DATATYPE: 'CURR', + LENG: '000015', + DECIMALS: '000002', + REFTABLE: 'ZTEST', + REFFIELD: 'F_CUKY', + }, + { + FIELDNAME: 'F_QUAN', + POSITION: '0003', + DATATYPE: 'QUAN', + LENG: '000013', + DECIMALS: '000003', + REFTABLE: 'ZTEST', + REFFIELD: 'F_UNIT', + }, + { + FIELDNAME: 'F_CUKY', + POSITION: '0004', + DATATYPE: 'CUKY', + LENG: '000005', + }, + { + FIELDNAME: 'F_UNIT', + POSITION: '0005', + DATATYPE: 'UNIT', + LENG: '000003', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('f_dec : abap.dec(15,2)'); + expect(result).toContain('f_curr : abap.curr(15,2)'); + expect(result).toContain('f_quan : abap.quan(13,3)'); + }); + + it('should map variable-length types (string, rawstring)', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { FIELDNAME: 'F_STRING', POSITION: '0001', DATATYPE: 'STRG' }, + { FIELDNAME: 'F_RSTR', POSITION: '0002', DATATYPE: 'RSTR' }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('f_string : abap.string(0)'); + expect(result).toContain('f_rstr : abap.rawstring(0)'); + }); + }); + + describe('data element references', () => { + it('should emit data element name for COMPTYPE=E fields', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'COUNTRY_CODE', + POSITION: '0001', + ROLLNAME: 'LAND1', + COMPTYPE: 'E', + }, + { + FIELDNAME: 'LANGUAGE', + POSITION: '0002', + ROLLNAME: 'SPRAS', + COMPTYPE: 'E', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('country_code : land1'); + expect(result).toContain('language : spras'); + }); + }); + + describe('currency/quantity annotations', () => { + it('should emit @Semantics.amount.currencyCode for CURR fields', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'AMOUNT', + POSITION: '0001', + DATATYPE: 'CURR', + LENG: '000015', + DECIMALS: '000002', + REFTABLE: 'ZTEST', + REFFIELD: 'CURRENCY', + }, + { + FIELDNAME: 'CURRENCY', + POSITION: '0002', + DATATYPE: 'CUKY', + LENG: '000005', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain( + "@Semantics.amount.currencyCode : 'ztest.currency'", + ); + expect(result).toContain('amount : abap.curr(15,2)'); + }); + + it('should emit @Semantics.quantity.unitOfMeasure for QUAN fields', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'QUANTITY', + POSITION: '0001', + DATATYPE: 'QUAN', + LENG: '000013', + DECIMALS: '000003', + REFTABLE: 'ZTEST', + REFFIELD: 'UOM', + }, + { + FIELDNAME: 'UOM', + POSITION: '0002', + DATATYPE: 'UNIT', + LENG: '000003', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain( + "@Semantics.quantity.unitOfMeasure : 'ztest.uom'", + ); + expect(result).toContain('quantity : abap.quan(13,3)'); + }); + }); + + describe('includes', () => { + it('should emit include directive for .INCLUDE entries', () => { + const dd02v: DD02VData = { TABNAME: 'ZTEST', TABCLASS: 'INTTAB' }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'FIELD1', + POSITION: '0001', + DATATYPE: 'CHAR', + LENG: '000010', + }, + { + FIELDNAME: '.INCLUDE', + POSITION: '0002', + PRECFIELD: 'ZOTHER_STRUCT', + MASK: ' S', + COMPTYPE: 'S', + }, + { + FIELDNAME: '.INCLU-_XX', + POSITION: '0003', + PRECFIELD: 'ZOTHER_STRUCT', + MASK: ' S', + COMPTYPE: 'S', + }, + { + FIELDNAME: 'FIELD2', + POSITION: '0004', + DATATYPE: 'NUMC', + LENG: '000005', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toContain('field1 : abap.char(10)'); + expect(result).toContain('include zother_struct;'); + expect(result).toContain('field2 : abap.numc(5)'); + // Should NOT contain .INCLU-_XX as a separate line + expect(result).not.toContain('inclu-'); + }); + }); + + describe('key fields and not null', () => { + it('should emit key keyword and not null for key fields', () => { + const dd02v: DD02VData = { + TABNAME: 'ZTEST', + TABCLASS: 'TRANSP', + CONTFLAG: 'A', + }; + const dd03p: DD03PData[] = [ + { + FIELDNAME: 'KEY1', + POSITION: '0001', + KEYFLAG: 'X', + DATATYPE: 'CHAR', + LENG: '000010', + NOTNULL: 'X', + }, + { + FIELDNAME: 'DATA1', + POSITION: '0002', + DATATYPE: 'CHAR', + LENG: '000040', + }, + ]; + + const result = buildCdsDdl(dd02v, dd03p); + expect(result).toMatch(/key key1\s+: abap\.char\(10\) not null/); + expect(result).toMatch(/data1\s+: abap\.char\(40\)/); + expect(result).not.toMatch(/data1\s+: abap\.char\(40\) not null/); + }); + }); + + describe('enhancement category mapping', () => { + it.each([ + ['0', '#NOT_CLASSIFIED'], + ['1', '#NOT_EXTENSIBLE'], + ['2', '#EXTENSIBLE_CHARACTER_NUMERIC'], + ['3', '#EXTENSIBLE_CHARACTER'], + ['4', '#EXTENSIBLE_ANY'], + ])('should map EXCLASS %s to %s', (exclass, expected) => { + const dd02v: DD02VData = { + TABNAME: 'ZTEST', + TABCLASS: 'INTTAB', + EXCLASS: exclass, + }; + const result = buildCdsDdl(dd02v, []); + expect(result).toContain( + `@AbapCatalog.enhancement.category : ${expected}`, + ); + }); + }); +}); + +// ============================================ +// XML parsing tests +// ============================================ + +describe('parseTablXml', () => { + it('should parse zage_structure.tabl.xml', () => { + const xml = loadFixture('zage_structure.tabl.xml'); + const { dd02v, dd03p } = parseTablXml(xml); + + expect(dd02v.TABNAME).toBe('ZAGE_STRUCTURE'); + expect(dd02v.TABCLASS).toBe('INTTAB'); + expect(dd02v.DDTEXT).toBe('Simple structure'); + expect(dd02v.EXCLASS).toBe('4'); + + expect(dd03p).toBeInstanceOf(Array); + expect(dd03p.length).toBeGreaterThan(0); + + // Check first field + expect(dd03p[0].FIELDNAME).toBe('CHAR_FIELD'); + expect(dd03p[0].DATATYPE).toBe('CHAR'); + expect(dd03p[0].LENG).toBe('000010'); + }); + + it('should parse zage_tabl.tabl.xml (transparent table)', () => { + const xml = loadFixture('zage_tabl.tabl.xml'); + const { dd02v, dd03p } = parseTablXml(xml); + + expect(dd02v.TABNAME).toBe('ZAGE_TABL'); + expect(dd02v.TABCLASS).toBe('TRANSP'); + expect(dd02v.CONTFLAG).toBe('A'); + + // Has key fields + const keyFields = dd03p.filter((f) => f.KEYFLAG === 'X'); + expect(keyFields.length).toBe(2); + }); +}); + +// ============================================ +// Integration tests: tablXmlToCdsDdl +// ============================================ + +describe('tablXmlToCdsDdl', () => { + it('should convert zage_structure.tabl.xml to CDS DDL', () => { + const xml = loadFixture('zage_structure.tabl.xml'); + const ddl = tablXmlToCdsDdl(xml); + + // Structure header + expect(ddl).toContain('define structure zage_structure'); + expect(ddl).toContain("@EndUserText.label : 'Simple structure'"); + expect(ddl).toContain( + '@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY', + ); + + // Built-in type fields + expect(ddl).toContain('char_field'); + expect(ddl).toContain('abap.char(10)'); + expect(ddl).toContain('numc_field'); + expect(ddl).toContain('abap.numc(5)'); + expect(ddl).toContain('int1_field'); + expect(ddl).toContain('abap.int1'); + expect(ddl).toContain('int2_field'); + expect(ddl).toContain('abap.int2'); + expect(ddl).toContain('int4_field'); + expect(ddl).toContain('abap.int4'); + expect(ddl).toContain('int8_field'); + expect(ddl).toContain('abap.int8'); + expect(ddl).toContain('fltp_field'); + expect(ddl).toContain('abap.fltp'); + expect(ddl).toContain('dats_field'); + expect(ddl).toContain('abap.dats'); + expect(ddl).toContain('tims_field'); + expect(ddl).toContain('abap.tims'); + expect(ddl).toContain('datn_field'); + expect(ddl).toContain('abap.datn'); + expect(ddl).toContain('timn_field'); + expect(ddl).toContain('abap.timn'); + expect(ddl).toContain('utclong_field'); + expect(ddl).toContain('abap.utclong'); + expect(ddl).toContain('raw_field'); + expect(ddl).toContain('abap.raw(16)'); + expect(ddl).toContain('string_field'); + expect(ddl).toContain('abap.string(0)'); + expect(ddl).toContain('rawstring_field'); + expect(ddl).toContain('abap.rawstring(0)'); + + // Decimal types + expect(ddl).toContain('dec_field'); + expect(ddl).toContain('abap.dec(15,2)'); + + // Currency/quantity with annotations + expect(ddl).toContain( + "@Semantics.amount.currencyCode : 'zage_structure.currency_code'", + ); + expect(ddl).toContain('curr_field'); + expect(ddl).toContain('abap.curr(15,2)'); + expect(ddl).toContain( + "@Semantics.quantity.unitOfMeasure : 'zage_structure.unit_code'", + ); + expect(ddl).toContain('quan_field'); + expect(ddl).toContain('abap.quan(13,3)'); + + // Reference fields + expect(ddl).toContain('currency_code'); + expect(ddl).toContain('abap.cuky(5)'); + expect(ddl).toContain('unit_code'); + expect(ddl).toContain('abap.unit(2)'); + + // Data element fields + expect(ddl).toContain('country_code'); + expect(ddl).toContain('land1'); + expect(ddl).toContain('language'); + expect(ddl).toContain('spras'); + expect(ddl).toContain('client'); + expect(ddl).toContain('mandt'); + + // Include + expect(ddl).toContain('include zage_structure1'); + + // Should NOT have table-specific annotations (it's a structure) + expect(ddl).not.toContain('@AbapCatalog.tableCategory'); + expect(ddl).not.toContain('@AbapCatalog.deliveryClass'); + }); + + it('should convert zage_tabl.tabl.xml to CDS DDL', () => { + const xml = loadFixture('zage_tabl.tabl.xml'); + const ddl = tablXmlToCdsDdl(xml); + + // Table header + expect(ddl).toContain('define table zage_tabl'); + expect(ddl).toContain("@EndUserText.label : 'AGE Test Transparent Table'"); + expect(ddl).toContain('@AbapCatalog.tableCategory : #TRANSPARENT'); + expect(ddl).toContain('@AbapCatalog.deliveryClass : #A'); + expect(ddl).toContain( + '@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE', + ); + + // Key fields + expect(ddl).toContain('key mandt'); + expect(ddl).toContain('not null'); + expect(ddl).toContain('key key_field'); + }); + + it('should convert zage_structure1.tabl.xml to CDS DDL', () => { + const xml = loadFixture('zage_structure1.tabl.xml'); + const ddl = tablXmlToCdsDdl(xml); + + expect(ddl).toContain('define structure zage_structure1'); + expect(ddl).toContain("@EndUserText.label : 'Simple structure'"); + expect(ddl).toContain( + '@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE', + ); + expect(ddl).toContain('component_to_be_changed : abap.string(0)'); + }); + + it('should convert zage_value_table.tabl.xml to CDS DDL', () => { + const xml = loadFixture('zage_value_table.tabl.xml'); + const ddl = tablXmlToCdsDdl(xml); + + expect(ddl).toContain('define table zage_value_table'); + expect(ddl).toContain('@AbapCatalog.tableCategory : #TRANSPARENT'); + expect(ddl).toContain('@AbapCatalog.deliveryClass : #A'); + + // Key fields + expect(ddl).toContain('key client'); + expect(ddl).toContain('abap.clnt'); + expect(ddl).toContain('not null'); + expect(ddl).toContain('key db_key'); + expect(ddl).toContain('zage_dtel_value_table_key'); + }); +}); diff --git a/packages/adt-diff/tsconfig.json b/packages/adt-diff/tsconfig.json new file mode 100644 index 00000000..6ffae8ab --- /dev/null +++ b/packages/adt-diff/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "bundler", + "module": "ESNext", + "target": "ES2022" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"], + "references": [ + { + "path": "../adt-plugin-abapgit" + }, + { + "path": "../adk" + }, + { + "path": "../adt-plugin" + } + ] +} diff --git a/packages/adt-diff/tsdown.config.ts b/packages/adt-diff/tsdown.config.ts new file mode 100644 index 00000000..7c396e1a --- /dev/null +++ b/packages/adt-diff/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts', 'src/commands/diff.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + external: [/^@abapify\//, 'chalk', 'diff', 'fast-xml-parser'], +}); diff --git a/packages/adt-diff/vitest.config.ts b/packages/adt-diff/vitest.config.ts new file mode 100644 index 00000000..4a58023e --- /dev/null +++ b/packages/adt-diff/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + }, +}); diff --git a/packages/adt-plugin-abapgit/src/index.ts b/packages/adt-plugin-abapgit/src/index.ts index fd9a2eca..52370fa5 100644 --- a/packages/adt-plugin-abapgit/src/index.ts +++ b/packages/adt-plugin-abapgit/src/index.ts @@ -4,6 +4,17 @@ export { abapGitPlugin, AbapGitPlugin } from './lib/abapgit'; // Finding resolver for ATC integration export { createFindingResolver } from './lib/finding-resolver'; +// Handler registry — for consumers that need to work with handlers directly +export { + getHandler, + isSupported, + getSupportedTypes, +} from './lib/handlers/registry'; +export type { SerializedFile, ObjectHandler } from './lib/handlers/base'; + +// Filename parser — shared utility for abapGit file naming convention +export { parseAbapGitFilename } from './lib/deserializer'; + // Re-export types from @abapify/adt-plugin for convenience export type { AdtPlugin, diff --git a/packages/adt-plugin-abapgit/src/lib/deserializer.ts b/packages/adt-plugin-abapgit/src/lib/deserializer.ts index 39e1912c..8f9c16d1 100644 --- a/packages/adt-plugin-abapgit/src/lib/deserializer.ts +++ b/packages/adt-plugin-abapgit/src/lib/deserializer.ts @@ -27,7 +27,7 @@ import { /** * Parse abapGit filename to extract object info */ -function parseAbapGitFilename(filename: string): { +export function parseAbapGitFilename(filename: string): { name: string; type: string; suffix?: string; diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index 21079d29..fdf25d58 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -11,6 +11,7 @@ import type { Annotation, AnnotationValue, FieldDefinition, + IncludeDirective, BuiltinTypeRef, TableMember, } from '@abapify/acds'; @@ -23,10 +24,12 @@ export interface DD02VData { TABNAME?: string; DDLANGUAGE?: string; TABCLASS?: string; + LANGDEP?: string; DDTEXT?: string; + MASTERLANG?: string; CONTFLAG?: string; EXCLASS?: string; - MASTERLANG?: string; + AUTHCLASS?: string; CLIDEP?: string; BUFFERED?: string; MATEFLAG?: string; @@ -38,14 +41,19 @@ export interface DD03PData { KEYFLAG?: string; ROLLNAME?: string; ADMINFIELD?: string; - NOTNULL?: string; - COMPTYPE?: string; INTTYPE?: string; INTLEN?: string; + NOTNULL?: string; DATATYPE?: string; LENG?: string; DECIMALS?: string; + SHLPORIGIN?: string; MASK?: string; + COMPTYPE?: string; + REFTABLE?: string; + REFFIELD?: string; + PRECFIELD?: string; + DDTEXT?: string; } // ============================================ @@ -142,11 +150,12 @@ export function buildDD02V( ); // Build result in standard abapGit DD02V field order: - // TABNAME, DDLANGUAGE, TABCLASS, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS + // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS const result: DD02VData = {}; result.TABNAME = def.name.toUpperCase(); result.DDLANGUAGE = language; result.TABCLASS = tabclass; + result.LANGDEP = 'X'; if (hasClientField) result.CLIDEP = 'X'; result.DDTEXT = description; result.MASTERLANG = language; @@ -164,58 +173,80 @@ export function buildDD02V( interface BuiltinType { datatype: string; inttype: string; + /** Fixed DDIC length (for types without user-specified length) */ length?: number; + /** Fixed DDIC decimals (e.g. FLTP always has 16) */ decimals?: number; + /** Fixed internal byte length (overrides computed INTLEN) */ fixedIntlen?: number; + /** Whether this type has no LENG in the DDIC (variable-length types) */ + noLeng?: boolean; + /** SHLPORIGIN value for search help (T = table-driven) */ + shlporigin?: string; } +// DDIC INTTYPE/INTLEN mapping based on SAP abapGit serialization: +// - INT1/INT2/INT4 use INTTYPE=X with raw byte-size INTLEN +// - DATS/TIMS/DATN/TIMN use Unicode-doubled INTLEN +// - String/rawstring have fixedIntlen=8 and no LENG const BUILTIN_TYPES: Record = { char: { datatype: 'CHAR', inttype: 'C' }, clnt: { datatype: 'CLNT', inttype: 'C', length: 3 }, numc: { datatype: 'NUMC', inttype: 'N' }, - dats: { datatype: 'DATS', inttype: 'D', length: 8 }, - tims: { datatype: 'TIMS', inttype: 'T', length: 6 }, - string: { datatype: 'STRG', inttype: 'g', fixedIntlen: 8 }, - xstring: { datatype: 'RSTR', inttype: 'h', fixedIntlen: 8 }, - int1: { datatype: 'INT1', inttype: 'b', fixedIntlen: 3 }, - int2: { datatype: 'INT2', inttype: 's', fixedIntlen: 5 }, - int4: { datatype: 'INT4', inttype: 'I', fixedIntlen: 4 }, - int8: { datatype: 'INT8', inttype: '8', fixedIntlen: 8 }, - fltp: { datatype: 'FLTP', inttype: 'F', fixedIntlen: 8 }, + dats: { + datatype: 'DATS', + inttype: 'D', + length: 8, + fixedIntlen: 16, + shlporigin: 'T', + }, + datn: { + datatype: 'DATN', + inttype: 'D', + length: 8, + fixedIntlen: 16, + shlporigin: 'T', + }, + tims: { + datatype: 'TIMS', + inttype: 'T', + length: 6, + fixedIntlen: 12, + shlporigin: 'T', + }, + timn: { + datatype: 'TIMN', + inttype: 'T', + length: 6, + fixedIntlen: 12, + shlporigin: 'T', + }, + string: { datatype: 'STRG', inttype: 'g', fixedIntlen: 8, noLeng: true }, + xstring: { datatype: 'RSTR', inttype: 'y', fixedIntlen: 8, noLeng: true }, + int1: { datatype: 'INT1', inttype: 'X', length: 3, fixedIntlen: 1 }, + int2: { datatype: 'INT2', inttype: 'X', length: 5, fixedIntlen: 2 }, + int4: { datatype: 'INT4', inttype: 'X', length: 10, fixedIntlen: 4 }, + int8: { datatype: 'INT8', inttype: '8', length: 19, fixedIntlen: 8 }, + fltp: { + datatype: 'FLTP', + inttype: 'F', + length: 16, + decimals: 16, + fixedIntlen: 8, + }, dec: { datatype: 'DEC', inttype: 'P' }, curr: { datatype: 'CURR', inttype: 'P' }, quan: { datatype: 'QUAN', inttype: 'P' }, raw: { datatype: 'RAW', inttype: 'X' }, - rawstring: { datatype: 'RSTR', inttype: 'h', fixedIntlen: 8 }, + rawstring: { datatype: 'RSTR', inttype: 'y', fixedIntlen: 8, noLeng: true }, lang: { datatype: 'LANG', inttype: 'C', length: 1 }, - unit: { datatype: 'UNIT', inttype: 'C' }, - cuky: { datatype: 'CUKY', inttype: 'C' }, + unit: { datatype: 'UNIT', inttype: 'C', length: 3 }, + cuky: { datatype: 'CUKY', inttype: 'C', length: 5 }, d16n: { datatype: 'D16N', inttype: 'a', fixedIntlen: 8 }, d34n: { datatype: 'D34N', inttype: 'e', fixedIntlen: 16 }, - utclong: { datatype: 'UTCL', inttype: 'p', fixedIntlen: 8 }, + utclong: { datatype: 'UTCL', inttype: 'p', length: 27, fixedIntlen: 8 }, }; -/** - * Types that always get a MASK (2 spaces + DATATYPE) - */ -const MASK_TYPES = new Set([ - 'clnt', - 'string', - 'xstring', - 'rawstring', - 'lang', - 'unit', - 'cuky', - 'dats', - 'tims', - 'dec', - 'curr', - 'quan', - 'd16n', - 'd34n', - 'utclong', -]); - /** Zero-pad number to given width */ function zeroPad(n: number, width: number): string { return String(n).padStart(width, '0'); @@ -249,7 +280,11 @@ function computeIntlen(builtin: BuiltinType, length?: number): number { } /** Build a DD03P entry from a single field definition */ -function buildFieldDD03P(field: FieldDefinition): DD03PData { +function buildFieldDD03P( + field: FieldDefinition, + position: number, + tableName: string, +): DD03PData { // Compute all values first const isKey = field.isKey; const notNull = field.notNull; @@ -261,18 +296,52 @@ function buildFieldDD03P(field: FieldDefinition): DD03PData { let leng: string | undefined; let decimals: string | undefined; let mask: string | undefined; + let shlporigin: string | undefined; + let reftable: string | undefined; + let reffield: string | undefined; if (field.type.kind === 'builtin') { const bt = field.type as BuiltinTypeRef; const typeInfo = BUILTIN_TYPES[bt.name]; if (typeInfo) { + // Use explicit CDS length, or fall back to fixed DDIC length const length = bt.length ?? typeInfo.length; inttype = typeInfo.inttype; intlen = zeroPad(computeIntlen(typeInfo, length), 6); datatype = typeInfo.datatype; - if (length !== undefined) leng = zeroPad(length, 6); - if (bt.decimals !== undefined) decimals = zeroPad(bt.decimals, 6); - if (MASK_TYPES.has(bt.name)) mask = ` ${typeInfo.datatype}`; + + // LENG: skip for variable-length types (string, xstring, rawstring) + if (!typeInfo.noLeng && length !== undefined) { + leng = zeroPad(length, 6); + } + + // DECIMALS: use explicit CDS decimals, or fixed DDIC decimals + const dec = bt.decimals ?? typeInfo.decimals; + if (dec !== undefined) decimals = zeroPad(dec, 6); + + // MASK: all builtin types get " DATATYPE" + mask = ` ${typeInfo.datatype}`; + + // SHLPORIGIN (date/time types) + if (typeInfo.shlporigin) shlporigin = typeInfo.shlporigin; + + // REFTABLE/REFFIELD for currency and quantity fields + // These come from CDS annotations on the field + // Annotation value is 'tablename.fieldname' — we only need the field part + if (bt.name === 'curr' || bt.name === 'quan') { + const refAnnotation = getAnnotation( + field.annotations, + bt.name === 'curr' + ? 'Semantics.amount.currencyCode' + : 'Semantics.quantity.unitOfMeasure', + ); + if (refAnnotation) { + reftable = tableName; + // Strip table prefix if present (e.g. 'ztest.currency_code' → 'CURRENCY_CODE') + const parts = refAnnotation.split('.'); + reffield = parts[parts.length - 1].toUpperCase(); + } + } } } else { // Named type (data element reference) @@ -280,10 +349,10 @@ function buildFieldDD03P(field: FieldDefinition): DD03PData { comptype = 'E'; } - // Build in standard abapGit DD03P field order: - // FIELDNAME, KEYFLAG, ROLLNAME, ADMINFIELD, INTTYPE, INTLEN, NOTNULL, DATATYPE, LENG, DECIMALS, MASK, COMPTYPE + // Build in standard abapGit DD03P field order const result: DD03PData = {}; result.FIELDNAME = field.name.toUpperCase(); + result.POSITION = zeroPad(position, 4); if (isKey) result.KEYFLAG = 'X'; if (rollname) result.ROLLNAME = rollname; result.ADMINFIELD = '0'; @@ -293,6 +362,9 @@ function buildFieldDD03P(field: FieldDefinition): DD03PData { if (datatype) result.DATATYPE = datatype; if (leng) result.LENG = leng; if (decimals) result.DECIMALS = decimals; + if (shlporigin) result.SHLPORIGIN = shlporigin; + if (reftable) result.REFTABLE = reftable; + if (reffield) result.REFFIELD = reffield; if (mask) result.MASK = mask; if (comptype) result.COMPTYPE = comptype; @@ -302,9 +374,47 @@ function buildFieldDD03P(field: FieldDefinition): DD03PData { /** * Build DD03P entries from table/structure members */ -export function buildDD03P(members: TableMember[]): DD03PData[] { - const fields = members.filter( - (m): m is FieldDefinition => !('kind' in m && m.kind === 'include'), - ); - return fields.map((field) => buildFieldDD03P(field)); +export function buildDD03P( + members: TableMember[], + tableName: string = '', +): DD03PData[] { + const entries: DD03PData[] = []; + let position = 0; + + for (const member of members) { + if ('kind' in member && member.kind === 'include') { + const inc = member as IncludeDirective; + const includeName = inc.name.toUpperCase(); + position++; + + if (inc.suffix) { + // Include with suffix → .INCLU- entry + entries.push({ + FIELDNAME: `.INCLU-${inc.suffix.toUpperCase()}`, + POSITION: zeroPad(position, 4), + ADMINFIELD: '0', + PRECFIELD: includeName, + MASK: ' S', + COMPTYPE: 'S', + }); + } else { + // Plain include → .INCLUDE entry + entries.push({ + FIELDNAME: '.INCLUDE', + POSITION: zeroPad(position, 4), + ADMINFIELD: '0', + PRECFIELD: includeName, + MASK: ' S', + COMPTYPE: 'S', + }); + } + } else { + // Regular field definition + const field = member as FieldDefinition; + position++; + entries.push(buildFieldDD03P(field, position, tableName)); + } + } + + return entries; } diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts index 1bc3268c..96dc8d31 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts @@ -85,7 +85,7 @@ async function serializeTabl( const dd02v = buildDD02V(def, lang, obj.description ?? ''); // Build DD03P from AST field definitions - const dd03pEntries = buildDD03P(def.members); + const dd03pEntries = buildDD03P(def.members, def.name.toUpperCase()); // Construct the full abapGit values const values: Record = { diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts index 62320c52..1edf76a5 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts @@ -91,6 +91,11 @@ export default { type: 'xs:string', minOccurs: '0', }, + { + name: 'LANGDEP', + type: 'xs:string', + minOccurs: '0', + }, { name: 'CLIDEP', type: 'xs:string', diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts index e8b62361..2d1ef8a8 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts @@ -14,6 +14,7 @@ export type TablSchema = TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; + LANGDEP?: string; CLIDEP?: string; SQLTAB?: string; DATCLASS?: string; @@ -66,6 +67,7 @@ export type TablSchema = TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; + LANGDEP?: string; CLIDEP?: string; SQLTAB?: string; DATCLASS?: string; @@ -113,6 +115,7 @@ export type TablSchema = TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; + LANGDEP?: string; CLIDEP?: string; SQLTAB?: string; DATCLASS?: string; diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts index d310863a..fb975a1b 100644 --- a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -45,6 +45,55 @@ define table zage_transparent_table { } `; +/** Structure with ALL builtin field types (matches zage_structure.tabl.xml fixture) */ +const CDS_ALL_TYPES = ` +@EndUserText.label : 'Simple structure' +@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY +define structure zage_structure { + char_field : abap.char(10); + numc_field : abap.numc(5); + string_field : abap.string; + int1_field : abap.int1; + int2_field : abap.int2; + int4_field : abap.int4; + int8_field : abap.int8; + dec_field : abap.dec(15,2); + fltp_field : abap.fltp; + dats_field : abap.dats; + tims_field : abap.tims; + raw_field : abap.raw(16); + rawstring_field : abap.rawstring; + currency_code : abap.cuky(5); + unit_code : abap.unit(2); + utclong_field : abap.utclong; + country_code : land1; + language : spras; + client : mandt; +} +`; + +/** Structure with include directives */ +const CDS_WITH_INCLUDES = ` +@EndUserText.label : 'Structure with includes' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +define structure ztest_with_include { + field1 : abap.char(10); + include zage_structure1; + field2 : abap.numc(5); +} +`; + +/** Structure with include + suffix directives */ +const CDS_WITH_INCLUDES_SUFFIX = ` +@EndUserText.label : 'Structure with includes and suffix' +define structure ztest_include_suffix { + field1 : abap.char(10); + include zage_structure1; + include zage_structure1 with suffix _xx; + field2 : abap.numc(5); +} +`; + /** Table with data element references */ const CDS_TABLE_DATA_ELEMENTS = ` @EndUserText.label : 'Table with data elements' @@ -370,3 +419,257 @@ describe('CDS-to-abapGit mapping', () => { assert.strictEqual(valueField.NOTNULL, undefined); }); }); + +describe('CDS-to-abapGit field type mapping (all builtin types)', () => { + // Parse once, reuse for all type-specific tests + const { ast } = parse(CDS_ALL_TYPES); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members, 'ZAGE_STRUCTURE'); + + /** Helper to find entry by field name */ + function field(name: string) { + const entry = entries.find((e) => e.FIELDNAME === name); + assert.ok(entry, `Field ${name} should exist in DD03P entries`); + return entry!; + } + + it('char(10): INTTYPE=C, INTLEN=000020, DATATYPE=CHAR, LENG=000010', () => { + const f = field('CHAR_FIELD'); + assert.strictEqual(f.INTTYPE, 'C'); + assert.strictEqual(f.INTLEN, '000020'); + assert.strictEqual(f.DATATYPE, 'CHAR'); + assert.strictEqual(f.LENG, '000010'); + assert.strictEqual(f.MASK, ' CHAR'); + assert.strictEqual(f.POSITION, '0001'); + }); + + it('numc(5): INTTYPE=N, INTLEN=000010, DATATYPE=NUMC, LENG=000005', () => { + const f = field('NUMC_FIELD'); + assert.strictEqual(f.INTTYPE, 'N'); + assert.strictEqual(f.INTLEN, '000010'); + assert.strictEqual(f.DATATYPE, 'NUMC'); + assert.strictEqual(f.LENG, '000005'); + assert.strictEqual(f.MASK, ' NUMC'); + }); + + it('string: INTTYPE=g, INTLEN=000008, DATATYPE=STRG, no LENG', () => { + const f = field('STRING_FIELD'); + assert.strictEqual(f.INTTYPE, 'g'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'STRG'); + assert.strictEqual(f.LENG, undefined, 'string should not have LENG'); + assert.strictEqual(f.MASK, ' STRG'); + }); + + it('int1: INTTYPE=X, INTLEN=000001, DATATYPE=INT1, LENG=000003', () => { + const f = field('INT1_FIELD'); + assert.strictEqual(f.INTTYPE, 'X'); + assert.strictEqual(f.INTLEN, '000001'); + assert.strictEqual(f.DATATYPE, 'INT1'); + assert.strictEqual(f.LENG, '000003'); + assert.strictEqual(f.MASK, ' INT1'); + }); + + it('int2: INTTYPE=X, INTLEN=000002, DATATYPE=INT2, LENG=000005', () => { + const f = field('INT2_FIELD'); + assert.strictEqual(f.INTTYPE, 'X'); + assert.strictEqual(f.INTLEN, '000002'); + assert.strictEqual(f.DATATYPE, 'INT2'); + assert.strictEqual(f.LENG, '000005'); + assert.strictEqual(f.MASK, ' INT2'); + }); + + it('int4: INTTYPE=X, INTLEN=000004, DATATYPE=INT4, LENG=000010', () => { + const f = field('INT4_FIELD'); + assert.strictEqual(f.INTTYPE, 'X'); + assert.strictEqual(f.INTLEN, '000004'); + assert.strictEqual(f.DATATYPE, 'INT4'); + assert.strictEqual(f.LENG, '000010'); + assert.strictEqual(f.MASK, ' INT4'); + }); + + it('int8: INTTYPE=8, INTLEN=000008, DATATYPE=INT8, LENG=000019', () => { + const f = field('INT8_FIELD'); + assert.strictEqual(f.INTTYPE, '8'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'INT8'); + assert.strictEqual(f.LENG, '000019'); + assert.strictEqual(f.MASK, ' INT8'); + }); + + it('dec(15,2): INTTYPE=P, INTLEN=000008, DATATYPE=DEC, LENG=000015, DECIMALS=000002', () => { + const f = field('DEC_FIELD'); + assert.strictEqual(f.INTTYPE, 'P'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'DEC'); + assert.strictEqual(f.LENG, '000015'); + assert.strictEqual(f.DECIMALS, '000002'); + assert.strictEqual(f.MASK, ' DEC'); + }); + + it('fltp: INTTYPE=F, INTLEN=000008, DATATYPE=FLTP, LENG=000016, DECIMALS=000016', () => { + const f = field('FLTP_FIELD'); + assert.strictEqual(f.INTTYPE, 'F'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'FLTP'); + assert.strictEqual(f.LENG, '000016'); + assert.strictEqual(f.DECIMALS, '000016'); + assert.strictEqual(f.MASK, ' FLTP'); + }); + + it('dats: INTTYPE=D, INTLEN=000016, DATATYPE=DATS, LENG=000008, SHLPORIGIN=T', () => { + const f = field('DATS_FIELD'); + assert.strictEqual(f.INTTYPE, 'D'); + assert.strictEqual(f.INTLEN, '000016'); + assert.strictEqual(f.DATATYPE, 'DATS'); + assert.strictEqual(f.LENG, '000008'); + assert.strictEqual(f.SHLPORIGIN, 'T'); + assert.strictEqual(f.MASK, ' DATS'); + }); + + it('tims: INTTYPE=T, INTLEN=000012, DATATYPE=TIMS, LENG=000006, SHLPORIGIN=T', () => { + const f = field('TIMS_FIELD'); + assert.strictEqual(f.INTTYPE, 'T'); + assert.strictEqual(f.INTLEN, '000012'); + assert.strictEqual(f.DATATYPE, 'TIMS'); + assert.strictEqual(f.LENG, '000006'); + assert.strictEqual(f.SHLPORIGIN, 'T'); + assert.strictEqual(f.MASK, ' TIMS'); + }); + + it('raw(16): INTTYPE=X, INTLEN=000016, DATATYPE=RAW, LENG=000016', () => { + const f = field('RAW_FIELD'); + assert.strictEqual(f.INTTYPE, 'X'); + assert.strictEqual(f.INTLEN, '000016'); + assert.strictEqual(f.DATATYPE, 'RAW'); + assert.strictEqual(f.LENG, '000016'); + assert.strictEqual(f.MASK, ' RAW'); + }); + + it('rawstring: INTTYPE=y, INTLEN=000008, DATATYPE=RSTR, no LENG', () => { + const f = field('RAWSTRING_FIELD'); + assert.strictEqual(f.INTTYPE, 'y'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'RSTR'); + assert.strictEqual(f.LENG, undefined, 'rawstring should not have LENG'); + assert.strictEqual(f.MASK, ' RSTR'); + }); + + it('cuky(5): INTTYPE=C, INTLEN=000010, DATATYPE=CUKY, LENG=000005', () => { + const f = field('CURRENCY_CODE'); + assert.strictEqual(f.INTTYPE, 'C'); + assert.strictEqual(f.INTLEN, '000010'); + assert.strictEqual(f.DATATYPE, 'CUKY'); + assert.strictEqual(f.LENG, '000005'); + assert.strictEqual(f.MASK, ' CUKY'); + }); + + it('unit(2): INTTYPE=C, INTLEN=000004, DATATYPE=UNIT, LENG=000002', () => { + const f = field('UNIT_CODE'); + assert.strictEqual(f.INTTYPE, 'C'); + assert.strictEqual(f.INTLEN, '000004'); + assert.strictEqual(f.DATATYPE, 'UNIT'); + assert.strictEqual(f.LENG, '000002'); + assert.strictEqual(f.MASK, ' UNIT'); + }); + + it('utclong: INTTYPE=p, INTLEN=000008, DATATYPE=UTCL, LENG=000027', () => { + const f = field('UTCLONG_FIELD'); + assert.strictEqual(f.INTTYPE, 'p'); + assert.strictEqual(f.INTLEN, '000008'); + assert.strictEqual(f.DATATYPE, 'UTCL'); + assert.strictEqual(f.LENG, '000027'); + assert.strictEqual(f.MASK, ' UTCL'); + }); + + it('named type (data element): ROLLNAME, COMPTYPE=E, no INTTYPE/DATATYPE', () => { + const f = field('COUNTRY_CODE'); + assert.strictEqual(f.ROLLNAME, 'LAND1'); + assert.strictEqual(f.COMPTYPE, 'E'); + assert.strictEqual(f.INTTYPE, undefined); + assert.strictEqual(f.DATATYPE, undefined); + assert.strictEqual(f.LENG, undefined); + }); + + it('all fields have consecutive POSITION values', () => { + for (let i = 0; i < entries.length; i++) { + const expected = String(i + 1).padStart(4, '0'); + assert.strictEqual( + entries[i].POSITION, + expected, + `Field ${entries[i].FIELDNAME} should have POSITION=${expected}`, + ); + } + }); + + it('all fields have ADMINFIELD=0', () => { + for (const entry of entries) { + assert.strictEqual( + entry.ADMINFIELD, + '0', + `Field ${entry.FIELDNAME} should have ADMINFIELD=0`, + ); + } + }); +}); + +describe('CDS-to-abapGit DD02V LANGDEP', () => { + it('buildDD02V includes LANGDEP=X', () => { + const { ast } = parse(CDS_STRUCTURE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'AGE Test Structure'); + assert.strictEqual(dd02v.LANGDEP, 'X'); + }); +}); + +describe('CDS-to-abapGit include directives', () => { + it('generates .INCLUDE entry for plain include', () => { + const { ast } = parse(CDS_WITH_INCLUDES); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members); + + // field1 at position 1 + assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); + assert.strictEqual(entries[0].POSITION, '0001'); + + // .INCLUDE at position 2 + assert.strictEqual(entries[1].FIELDNAME, '.INCLUDE'); + assert.strictEqual(entries[1].POSITION, '0002'); + assert.strictEqual(entries[1].PRECFIELD, 'ZAGE_STRUCTURE1'); + assert.strictEqual(entries[1].MASK, ' S'); + assert.strictEqual(entries[1].COMPTYPE, 'S'); + + // field2 at position 3 + assert.strictEqual(entries[2].FIELDNAME, 'FIELD2'); + assert.strictEqual(entries[2].POSITION, '0003'); + + assert.strictEqual(entries.length, 3); + }); + + it('generates .INCLUDE and .INCLU- for include with suffix', () => { + const { ast } = parse(CDS_WITH_INCLUDES_SUFFIX); + const def = ast.definitions[0] as any; + const entries = buildDD03P(def.members); + + // field1 at position 1 + assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); + assert.strictEqual(entries[0].POSITION, '0001'); + + // .INCLUDE at position 2 (plain include) + assert.strictEqual(entries[1].FIELDNAME, '.INCLUDE'); + assert.strictEqual(entries[1].POSITION, '0002'); + assert.strictEqual(entries[1].PRECFIELD, 'ZAGE_STRUCTURE1'); + + // .INCLU-_XX at position 3 (include with suffix _xx) + assert.strictEqual(entries[2].FIELDNAME, '.INCLU-_XX'); + assert.strictEqual(entries[2].POSITION, '0003'); + assert.strictEqual(entries[2].PRECFIELD, 'ZAGE_STRUCTURE1'); + assert.strictEqual(entries[2].COMPTYPE, 'S'); + + // field2 at position 4 + assert.strictEqual(entries[3].FIELDNAME, 'FIELD2'); + assert.strictEqual(entries[3].POSITION, '0004'); + + assert.strictEqual(entries.length, 4); + }); +}); diff --git a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd index 55c4e6a5..613ad9ab 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd @@ -8,6 +8,7 @@ + From a8198fa96f8fdf96ee501998d1ee7bd86413fded Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 00:41:53 +0100 Subject: [PATCH 04/19] feat: resolve DDIC metadata via ADT for zero-diff TABL serialization Add runtime type resolution to the TABL/structure abapGit serializer. Named type references (data elements, structures) are now resolved via ADT REST endpoints to determine: - COMPTYPE: 'E' (data element) vs 'S' (structure) - SHLPORIGIN: 'D' when data element has a search help - DATATYPE/MASK: 'STRU'/'STRUS' for structure references - DDTEXT: description text for include entries Implementation: - Add fetchText() utility to AdkObject base class for generic ADT GET - Add TypeResolver interface and wire it through buildDD03P (now async) - Add createAdtTypeResolver() factory that probes dataelements then structures endpoints with caching - Fix HTTP 406 by not sending Accept header (let server choose) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- packages/adk/src/base/model.ts | 27 +++++++ .../src/lib/handlers/cds-to-abapgit.ts | 71 +++++++++++++++--- .../src/lib/handlers/objects/tabl.ts | 72 ++++++++++++++++++- .../tests/handlers/tabl.test.ts | 29 ++++---- 4 files changed, 174 insertions(+), 25 deletions(-) diff --git a/packages/adk/src/base/model.ts b/packages/adk/src/base/model.ts index 32068203..8a7dd111 100644 --- a/packages/adk/src/base/model.ts +++ b/packages/adk/src/base/model.ts @@ -849,6 +849,33 @@ export abstract class AdkObject { this.cache.delete(key); this.dirty.delete(key); } + + // ============================================ + // Public Fetch Utilities + // ============================================ + + /** + * Fetch a text resource from an ADT endpoint. + * Delegates to the underlying ADT client's fetch. + * + * @param url - ADT endpoint path (e.g., '/sap/bc/adt/ddic/dataelements/spras') + * @param headers - Optional HTTP headers (defaults to no Accept header, letting server choose) + * @returns Response content as text, or undefined if 404 + */ + async fetchText( + url: string, + headers?: Record, + ): Promise { + try { + const response = await this.ctx.client.fetch(url, { + method: 'GET', + headers: headers ?? {}, + }); + return toText(response); + } catch { + return undefined; + } + } } /** diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index fdf25d58..94e05ac8 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -3,6 +3,11 @@ * * Transforms @abapify/acds AST nodes into DD02V (header) and DD03P (field) * data structures for abapGit XML serialization. + * + * When a TypeResolver is provided, named type references are resolved via + * ADT to determine whether they are data elements or structures, and to + * extract metadata (search help origin, description) that is not available + * from the CDS source alone. */ import type { @@ -56,6 +61,28 @@ export interface DD03PData { DDTEXT?: string; } +// ============================================ +// Type Resolver — resolves named types via ADT +// ============================================ + +/** Result of resolving a named type reference */ +export interface ResolvedType { + /** 'E' = data element, 'S' = structure */ + comptype: 'E' | 'S'; + /** Search help origin (e.g. 'D' for domain search help) */ + shlporigin?: string; + /** Description text (used for include DDTEXT) */ + description?: string; +} + +/** + * Resolves named type references by querying ADT endpoints. + * Caches results to avoid duplicate HTTP requests. + */ +export interface TypeResolver { + resolve(name: string): Promise; +} + // ============================================ // Annotation helpers // ============================================ @@ -280,11 +307,12 @@ function computeIntlen(builtin: BuiltinType, length?: number): number { } /** Build a DD03P entry from a single field definition */ -function buildFieldDD03P( +async function buildFieldDD03P( field: FieldDefinition, position: number, tableName: string, -): DD03PData { + resolver?: TypeResolver, +): Promise { // Compute all values first const isKey = field.isKey; const notNull = field.notNull; @@ -344,9 +372,18 @@ function buildFieldDD03P( } } } else { - // Named type (data element reference) + // Named type reference — resolve via ADT if available rollname = field.type.name.toUpperCase(); - comptype = 'E'; + comptype = 'E'; // Default: data element + if (resolver) { + const resolved = await resolver.resolve(field.type.name); + comptype = resolved.comptype; + if (resolved.shlporigin) shlporigin = resolved.shlporigin; + if (resolved.comptype === 'S') { + datatype = 'STRU'; + mask = ' STRUS'; + } + } } // Build in standard abapGit DD03P field order @@ -374,10 +411,11 @@ function buildFieldDD03P( /** * Build DD03P entries from table/structure members */ -export function buildDD03P( +export async function buildDD03P( members: TableMember[], tableName: string = '', -): DD03PData[] { + resolver?: TypeResolver, +): Promise { const entries: DD03PData[] = []; let position = 0; @@ -387,32 +425,43 @@ export function buildDD03P( const includeName = inc.name.toUpperCase(); position++; + // Resolve include description via ADT if available + let ddtext: string | undefined; + if (resolver) { + const resolved = await resolver.resolve(inc.name); + ddtext = resolved.description; + } + if (inc.suffix) { // Include with suffix → .INCLU- entry - entries.push({ + const entry: DD03PData = { FIELDNAME: `.INCLU-${inc.suffix.toUpperCase()}`, POSITION: zeroPad(position, 4), ADMINFIELD: '0', PRECFIELD: includeName, MASK: ' S', COMPTYPE: 'S', - }); + }; + if (ddtext) entry.DDTEXT = ddtext; + entries.push(entry); } else { // Plain include → .INCLUDE entry - entries.push({ + const entry: DD03PData = { FIELDNAME: '.INCLUDE', POSITION: zeroPad(position, 4), ADMINFIELD: '0', PRECFIELD: includeName, MASK: ' S', COMPTYPE: 'S', - }); + }; + if (ddtext) entry.DDTEXT = ddtext; + entries.push(entry); } } else { // Regular field definition const field = member as FieldDefinition; position++; - entries.push(buildFieldDD03P(field, position, tableName)); + entries.push(await buildFieldDD03P(field, position, tableName, resolver)); } } diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts index 96dc8d31..91804ae4 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/objects/tabl.ts @@ -14,6 +14,8 @@ * - source/main GET: CDS source with annotations and field definitions * → parsed via @abapify/acds into AST * → mapped into DD02V annotations and DD03P field entries + * - Per named type: /sap/bc/adt/ddic/dataelements/{name} or /structures/{name} + * → resolves COMPTYPE (E vs S), SHLPORIGIN, description (DDTEXT) */ import { @@ -26,6 +28,7 @@ import { tabl } from '../../../schemas/generated'; import { createHandler } from '../base'; import { isoToSapLang, sapLangToIso } from '../lang'; import { buildDD02V, buildDD03P } from '../cds-to-abapgit'; +import type { TypeResolver, ResolvedType } from '../cds-to-abapgit'; import type { AdkObject } from '../adk'; /** @@ -42,6 +45,64 @@ function stripEmpty>(obj: T): Partial { return result as Partial; } +/** + * Create a TypeResolver that resolves named types via ADT endpoints. + * Tries data element first, then structure. Caches results. + */ +function createAdtTypeResolver(obj: AdkTable | AdkStructure): TypeResolver { + const cache = new Map(); + + return { + async resolve(name: string): Promise { + const key = name.toLowerCase(); + if (cache.has(key)) return cache.get(key)!; + + // Try data element first + const dtelXml = await obj.fetchText( + `/sap/bc/adt/ddic/dataelements/${encodeURIComponent(key)}`, + ); + if (dtelXml) { + const result: ResolvedType = { comptype: 'E' }; + + // Extract searchHelp → SHLPORIGIN=D + const searchHelpMatch = dtelXml.match( + /[^<]+<\/dtel:searchHelp>/, + ); + if (searchHelpMatch) { + result.shlporigin = 'D'; + } + + // Extract description for potential reuse + const descMatch = dtelXml.match(/adtcore:description="([^"]+)"/); + if (descMatch) result.description = descMatch[1]; + + cache.set(key, result); + return result; + } + + // Try structure + const structXml = await obj.fetchText( + `/sap/bc/adt/ddic/structures/${encodeURIComponent(key)}`, + ); + if (structXml) { + const result: ResolvedType = { comptype: 'S' }; + + // Extract description for include DDTEXT + const descMatch = structXml.match(/adtcore:description="([^"]+)"/); + if (descMatch) result.description = descMatch[1]; + + cache.set(key, result); + return result; + } + + // Fallback: assume data element + const fallback: ResolvedType = { comptype: 'E' }; + cache.set(key, fallback); + return fallback; + }, + }; +} + /** * Shared serialize logic for tables and structures. * Both use CDS source → DD02V/DD03P mapping. @@ -84,8 +145,15 @@ async function serializeTabl( // Build DD02V from AST annotations const dd02v = buildDD02V(def, lang, obj.description ?? ''); - // Build DD03P from AST field definitions - const dd03pEntries = buildDD03P(def.members, def.name.toUpperCase()); + // Create type resolver for named type resolution via ADT + const resolver = createAdtTypeResolver(obj); + + // Build DD03P from AST field definitions (async for type resolution) + const dd03pEntries = await buildDD03P( + def.members, + def.name.toUpperCase(), + resolver, + ); // Construct the full abapGit values const values: Record = { diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts index fb975a1b..85a7de5c 100644 --- a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -5,7 +5,7 @@ * and mapped to abapGit DD02V/DD03P XML format. */ -import { describe, it } from 'node:test'; +import { describe, it, before } from 'node:test'; import assert from 'node:assert'; // Import handler to trigger registration @@ -133,6 +133,7 @@ function createMockTable(overrides?: { abapLanguageVersion: '', dataSync: { name, type, description, language, masterLanguage: language }, getSource: async () => cdsSource, + fetchText: async () => undefined, // No ADT server in tests }; } @@ -366,10 +367,10 @@ describe('CDS-to-abapGit mapping', () => { assert.strictEqual(dd02v.EXCLASS, '1'); }); - it('buildDD03P maps builtin type fields', () => { + it('buildDD03P maps builtin type fields', async () => { const { ast } = parse(CDS_STRUCTURE); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members); + const entries = await buildDD03P(def.members); assert.strictEqual(entries.length, 3); @@ -388,10 +389,10 @@ describe('CDS-to-abapGit mapping', () => { assert.strictEqual(entries[2].DECIMALS, '000002'); }); - it('buildDD03P maps data element references', () => { + it('buildDD03P maps data element references', async () => { const { ast } = parse(CDS_TRANSPARENT_TABLE); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members); + const entries = await buildDD03P(def.members); // MANDT — data element reference const mandt = entries[0]; @@ -402,10 +403,10 @@ describe('CDS-to-abapGit mapping', () => { assert.strictEqual(mandt.NOTNULL, 'X'); }); - it('buildDD03P maps key and not null flags', () => { + it('buildDD03P maps key and not null flags', async () => { const { ast } = parse(CDS_TRANSPARENT_TABLE); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members); + const entries = await buildDD03P(def.members); // key_field: key, not null, builtin const keyField = entries[1]; @@ -424,7 +425,11 @@ describe('CDS-to-abapGit field type mapping (all builtin types)', () => { // Parse once, reuse for all type-specific tests const { ast } = parse(CDS_ALL_TYPES); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members, 'ZAGE_STRUCTURE'); + let entries: Awaited>; + + before(async () => { + entries = await buildDD03P(def.members, 'ZAGE_STRUCTURE'); + }); /** Helper to find entry by field name */ function field(name: string) { @@ -623,10 +628,10 @@ describe('CDS-to-abapGit DD02V LANGDEP', () => { }); describe('CDS-to-abapGit include directives', () => { - it('generates .INCLUDE entry for plain include', () => { + it('generates .INCLUDE entry for plain include', async () => { const { ast } = parse(CDS_WITH_INCLUDES); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members); + const entries = await buildDD03P(def.members); // field1 at position 1 assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); @@ -646,10 +651,10 @@ describe('CDS-to-abapGit include directives', () => { assert.strictEqual(entries.length, 3); }); - it('generates .INCLUDE and .INCLU- for include with suffix', () => { + it('generates .INCLUDE and .INCLU- for include with suffix', async () => { const { ast } = parse(CDS_WITH_INCLUDES_SUFFIX); const def = ast.definitions[0] as any; - const entries = buildDD03P(def.members); + const entries = await buildDD03P(def.members); // field1 at position 1 assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); From 5ba5e65a79935fe2b7f8bd90b0514e174e6e8310 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 01:00:51 +0100 Subject: [PATCH 05/19] feat(adt-diff): add --format ddl option and fix CDS DDL generation - Add --format ddl option to adt diff command for DDL-level comparison - Fix CUKY type to omit length (SAP convention: cuky has implicit length 5) - Fix .INCLU- handling: emit 'include with suffix ;' - Add blank lines after opening brace and before closing brace - Add trailing newline to generated DDL output Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- packages/adt-diff/src/commands/diff.ts | 66 +++++++++++++++++++ packages/adt-diff/src/lib/abapgit-to-cds.ts | 29 ++++++-- .../adt-diff/tests/abapgit-to-cds.test.ts | 4 +- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/adt-diff/src/commands/diff.ts b/packages/adt-diff/src/commands/diff.ts index 1f0a4674..9054d02a 100644 --- a/packages/adt-diff/src/commands/diff.ts +++ b/packages/adt-diff/src/commands/diff.ts @@ -33,6 +33,7 @@ import { parseAbapGitFilename, type ObjectHandler, } from '@abapify/adt-plugin-abapgit'; +import { tablXmlToCdsDdl } from '../lib/abapgit-to-cds'; /** * Collect all local files belonging to one abapGit object. @@ -243,12 +244,19 @@ export const diffCommand: CliCommandPlugin = { description: 'Number of context lines in diff', default: '3', }, + { + flags: '-f, --format ', + description: + 'Comparison format: xml (default) or ddl (TABL only — compare CDS DDL source)', + default: 'xml', + }, ], async execute(args: Record, ctx: CliContext) { const filePath = args.file as string; const contextLines = parseInt(String(args.context ?? '3'), 10); const useColor = args.color !== false; + const format = String(args.format ?? 'xml').toLowerCase(); // Resolve file path const fullPath = resolve(ctx.cwd, filePath); @@ -274,6 +282,19 @@ export const diffCommand: CliCommandPlugin = { process.exit(1); } + // Validate --format option + if (format !== 'xml' && format !== 'ddl') { + ctx.logger.error(`Unknown format: ${format}. Supported: xml, ddl`); + process.exit(1); + } + + if (format === 'ddl' && parsed.type !== 'TABL') { + ctx.logger.error( + `DDL format is only supported for TABL objects. Got: ${parsed.type}`, + ); + process.exit(1); + } + // Look up handler from abapGit registry const handler = getHandler(parsed.type); if (!handler) { @@ -337,6 +358,51 @@ export const diffCommand: CliCommandPlugin = { process.exit(1); } + // ======================================== + // DDL format: compare CDS DDL source + // ======================================== + if (format === 'ddl') { + // Local: convert abapGit XML → CDS DDL + const localXml = readFileSync(fullPath, 'utf-8'); + const localDdl = tablXmlToCdsDdl(localXml); + + // Remote: fetch CDS source directly from SAP + let remoteDdl: string; + try { + remoteDdl = await remoteObj.getSource(); + } catch (error) { + ctx.logger.error( + `Failed to fetch remote DDL source: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } + + const ddlFile = `${objectName}.tabl.ddl`; + const diffFound = printDiff( + ddlFile, + ddlFile, + localDdl, + remoteDdl, + contextLines, + useColor, + ); + + console.log(''); + if (diffFound) { + console.log( + useColor ? chalk.red('Differences found.') : 'Differences found.', + ); + process.exit(1); + } else { + console.log( + useColor + ? chalk.green('No differences found. (DDL source identical)') + : 'No differences found. (DDL source identical)', + ); + } + return; + } + // Serialize remote using the same handler → produces SerializedFile[] let remoteFiles; try { diff --git a/packages/adt-diff/src/lib/abapgit-to-cds.ts b/packages/adt-diff/src/lib/abapgit-to-cds.ts index 9174e71d..67d42722 100644 --- a/packages/adt-diff/src/lib/abapgit-to-cds.ts +++ b/packages/adt-diff/src/lib/abapgit-to-cds.ts @@ -95,6 +95,7 @@ const DATATYPE_TO_CDS: Record = { /** Types that never take a length parameter in CDS */ const NO_LENGTH_TYPES = new Set([ 'clnt', + 'cuky', 'dats', 'datn', 'tims', @@ -215,6 +216,7 @@ export function buildCdsDdl( // --- Definition header --- const keyword = isStructure ? 'define structure' : 'define table'; lines.push(`${keyword} ${tableName.toLowerCase()} {`); + lines.push(''); // Blank line after opening brace (SAP format) // --- Sort fields by POSITION --- const sorted = [...dd03pEntries].sort((a, b) => { @@ -236,7 +238,11 @@ export function buildCdsDdl( // --- Emit fields --- for (const info of fieldInfos) { if (info.kind === 'include') { - lines.push(` include ${info.name};`); + if (info.suffix) { + lines.push(` include ${info.name} with suffix ${info.suffix};`); + } else { + lines.push(` include ${info.name};`); + } } else { // Field annotations (e.g., @Semantics.amount.currencyCode) for (const ann of info.annotations) { @@ -248,9 +254,10 @@ export function buildCdsDdl( } } + lines.push(''); // Blank line before closing brace (SAP format) lines.push('}'); - return lines.join('\n'); + return lines.join('\n') + '\n'; // Trailing newline (SAP format) } // ============================================ @@ -270,6 +277,8 @@ interface FieldInfo { interface IncludeInfo { kind: 'include'; name: string; + /** Optional suffix for `include ... with suffix ` */ + suffix?: string; } type MemberInfo = FieldInfo | IncludeInfo; @@ -287,12 +296,22 @@ function buildFieldInfos( const entry = entries[i]; const fieldName = entry.FIELDNAME ?? ''; - // Skip .INCLU-_XX entries (part of include pair, handled with .INCLUDE) - if (fieldName === '.INCLU-_XX' || fieldName.startsWith('.INCLU-')) { + // .INCLU- entries → include with suffix + if (fieldName.startsWith('.INCLU-')) { + const includeName = entry.PRECFIELD ?? ''; + if (includeName) { + // Extract suffix from .INCLU- (e.g., ".INCLU-_XX" → "_xx") + const suffix = fieldName.slice('.INCLU-'.length).toLowerCase(); + result.push({ + kind: 'include', + name: includeName.toLowerCase(), + suffix, + }); + } continue; } - // Include directive + // Include directive → plain include if (fieldName === '.INCLUDE') { const includeName = entry.PRECFIELD ?? ''; if (includeName) { diff --git a/packages/adt-diff/tests/abapgit-to-cds.test.ts b/packages/adt-diff/tests/abapgit-to-cds.test.ts index d88ae453..bbca8dde 100644 --- a/packages/adt-diff/tests/abapgit-to-cds.test.ts +++ b/packages/adt-diff/tests/abapgit-to-cds.test.ts @@ -258,7 +258,7 @@ describe('buildCdsDdl', () => { expect(result).toContain('f_char : abap.char(10)'); expect(result).toContain('f_numc : abap.numc(5)'); expect(result).toContain('f_raw : abap.raw(16)'); - expect(result).toContain('f_cuky : abap.cuky(5)'); + expect(result).toContain('f_cuky : abap.cuky'); expect(result).toContain('f_unit : abap.unit(2)'); }); @@ -599,7 +599,7 @@ describe('tablXmlToCdsDdl', () => { // Reference fields expect(ddl).toContain('currency_code'); - expect(ddl).toContain('abap.cuky(5)'); + expect(ddl).toContain('abap.cuky'); expect(ddl).toContain('unit_code'); expect(ddl).toContain('abap.unit(2)'); From 0d06b3ca40ea13ae1632e8f741854b66d2057cee Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 07:40:09 +0100 Subject: [PATCH 06/19] fix(cds-to-abapgit): stop emitting LANGDEP, CLIDEP, POSITION from CDS source These DD02V/DD03P fields are database values that abapGit reads from SAP. They cannot be reliably determined from CDS DDL source alone: - LANGDEP: language dependency flag (varies per table) - CLIDEP: client dependency flag (varies per table) - POSITION: field ordinal position (assigned by SAP runtime) Removing these eliminates false diffs in roundtrip comparison, where our serializer was emitting fields that the remote XML may or may not contain depending on actual SAP metadata. Generated with Devin Co-Authored-By: Devin --- .../src/lib/handlers/cds-to-abapgit.ts | 29 +++------ .../tests/handlers/tabl.test.ts | 62 +++++++++---------- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index 94e05ac8..b521db99 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -166,24 +166,16 @@ export function buildDD02V( 'AbapCatalog.enhancement.category', ); - // CLIDEP — set to 'X' if table has a client-type field - const fields = def.members.filter( - (m): m is FieldDefinition => !('kind' in m && m.kind === 'include'), - ); - const hasClientField = fields.some( - (f) => - (f.type.kind === 'builtin' && f.type.name === 'clnt') || - (f.type.kind === 'named' && f.type.name.toLowerCase() === 'mandt'), - ); - // Build result in standard abapGit DD02V field order: // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS + // + // NOTE: LANGDEP and CLIDEP are real DD02V database values that cannot be + // reliably determined from CDS source alone. abapGit reads them from SAP + // and only emits them when set. We omit them here to avoid roundtrip mismatches. const result: DD02VData = {}; result.TABNAME = def.name.toUpperCase(); result.DDLANGUAGE = language; result.TABCLASS = tabclass; - result.LANGDEP = 'X'; - if (hasClientField) result.CLIDEP = 'X'; result.DDTEXT = description; result.MASTERLANG = language; if (deliveryClass) result.CONTFLAG = deliveryClass; @@ -309,7 +301,6 @@ function computeIntlen(builtin: BuiltinType, length?: number): number { /** Build a DD03P entry from a single field definition */ async function buildFieldDD03P( field: FieldDefinition, - position: number, tableName: string, resolver?: TypeResolver, ): Promise { @@ -387,9 +378,12 @@ async function buildFieldDD03P( } // Build in standard abapGit DD03P field order + // + // NOTE: POSITION is a real DD03P database value. abapGit emits it + // inconsistently (only for some transparent tables). We omit it to + // avoid roundtrip mismatches — SAP auto-computes it on import. const result: DD03PData = {}; result.FIELDNAME = field.name.toUpperCase(); - result.POSITION = zeroPad(position, 4); if (isKey) result.KEYFLAG = 'X'; if (rollname) result.ROLLNAME = rollname; result.ADMINFIELD = '0'; @@ -417,13 +411,11 @@ export async function buildDD03P( resolver?: TypeResolver, ): Promise { const entries: DD03PData[] = []; - let position = 0; for (const member of members) { if ('kind' in member && member.kind === 'include') { const inc = member as IncludeDirective; const includeName = inc.name.toUpperCase(); - position++; // Resolve include description via ADT if available let ddtext: string | undefined; @@ -436,7 +428,6 @@ export async function buildDD03P( // Include with suffix → .INCLU- entry const entry: DD03PData = { FIELDNAME: `.INCLU-${inc.suffix.toUpperCase()}`, - POSITION: zeroPad(position, 4), ADMINFIELD: '0', PRECFIELD: includeName, MASK: ' S', @@ -448,7 +439,6 @@ export async function buildDD03P( // Plain include → .INCLUDE entry const entry: DD03PData = { FIELDNAME: '.INCLUDE', - POSITION: zeroPad(position, 4), ADMINFIELD: '0', PRECFIELD: includeName, MASK: ' S', @@ -460,8 +450,7 @@ export async function buildDD03P( } else { // Regular field definition const field = member as FieldDefinition; - position++; - entries.push(await buildFieldDD03P(field, position, tableName, resolver)); + entries.push(await buildFieldDD03P(field, tableName, resolver)); } } diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts index 85a7de5c..401b54f9 100644 --- a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -192,7 +192,7 @@ describe('TABL handler', () => { assert.ok(xml.includes('P')); }); - it('sets POSITION for fields', async () => { + it('does not emit POSITION (SAP auto-computes on import)', async () => { const mock = createMockTable({ name: 'ZAGE_STRUCTURE', type: 'TABL/DS', @@ -203,10 +203,9 @@ describe('TABL handler', () => { const files = await handler!.serialize(mock as any); const xml = files[0].content; - // All fields should have POSITION - assert.ok(xml.includes('0001')); - assert.ok(xml.includes('0002')); - assert.ok(xml.includes('0003')); + // POSITION should NOT be emitted — abapGit reads it from DD03P but + // our serializer cannot determine it from CDS source alone + assert.ok(!xml.includes('')); }); }); @@ -268,7 +267,7 @@ describe('TABL handler', () => { assert.ok(xml.includes('E')); }); - it('detects client-dependent table', async () => { + it('does not emit CLIDEP (cannot determine reliably from CDS)', async () => { const mock = createMockTable({ name: 'ZTABLE_DTEL', description: 'Table with data elements', @@ -278,7 +277,9 @@ describe('TABL handler', () => { const files = await handler!.serialize(mock as any); const xml = files[0].content; - assert.ok(xml.includes('X')); + // CLIDEP is a DD02V database value — abapGit reads it from SAP. + // We cannot reliably determine it from CDS source alone. + assert.ok(!xml.includes('')); }); }); @@ -379,7 +380,7 @@ describe('CDS-to-abapGit mapping', () => { assert.strictEqual(entries[0].INTTYPE, 'C'); assert.strictEqual(entries[0].DATATYPE, 'CHAR'); assert.strictEqual(entries[0].LENG, '000010'); - assert.strictEqual(entries[0].POSITION, '0001'); + assert.strictEqual(entries[0].POSITION, undefined); // AMOUNT: abap.curr(15,2) assert.strictEqual(entries[2].FIELDNAME, 'AMOUNT'); @@ -445,7 +446,7 @@ describe('CDS-to-abapGit field type mapping (all builtin types)', () => { assert.strictEqual(f.DATATYPE, 'CHAR'); assert.strictEqual(f.LENG, '000010'); assert.strictEqual(f.MASK, ' CHAR'); - assert.strictEqual(f.POSITION, '0001'); + assert.strictEqual(f.POSITION, undefined); }); it('numc(5): INTTYPE=N, INTLEN=000010, DATATYPE=NUMC, LENG=000005', () => { @@ -596,13 +597,12 @@ describe('CDS-to-abapGit field type mapping (all builtin types)', () => { assert.strictEqual(f.LENG, undefined); }); - it('all fields have consecutive POSITION values', () => { - for (let i = 0; i < entries.length; i++) { - const expected = String(i + 1).padStart(4, '0'); + it('no fields have POSITION (not emitted by our serializer)', () => { + for (const entry of entries) { assert.strictEqual( - entries[i].POSITION, - expected, - `Field ${entries[i].FIELDNAME} should have POSITION=${expected}`, + entry.POSITION, + undefined, + `Field ${entry.FIELDNAME} should not have POSITION`, ); } }); @@ -619,11 +619,11 @@ describe('CDS-to-abapGit field type mapping (all builtin types)', () => { }); describe('CDS-to-abapGit DD02V LANGDEP', () => { - it('buildDD02V includes LANGDEP=X', () => { + it('buildDD02V does not emit LANGDEP (cannot determine from CDS)', () => { const { ast } = parse(CDS_STRUCTURE); const def = ast.definitions[0] as any; const dd02v = buildDD02V(def, 'E', 'AGE Test Structure'); - assert.strictEqual(dd02v.LANGDEP, 'X'); + assert.strictEqual(dd02v.LANGDEP, undefined); }); }); @@ -633,20 +633,20 @@ describe('CDS-to-abapGit include directives', () => { const def = ast.definitions[0] as any; const entries = await buildDD03P(def.members); - // field1 at position 1 + // field1 assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); - assert.strictEqual(entries[0].POSITION, '0001'); + assert.strictEqual(entries[0].POSITION, undefined); - // .INCLUDE at position 2 + // .INCLUDE assert.strictEqual(entries[1].FIELDNAME, '.INCLUDE'); - assert.strictEqual(entries[1].POSITION, '0002'); + assert.strictEqual(entries[1].POSITION, undefined); assert.strictEqual(entries[1].PRECFIELD, 'ZAGE_STRUCTURE1'); assert.strictEqual(entries[1].MASK, ' S'); assert.strictEqual(entries[1].COMPTYPE, 'S'); - // field2 at position 3 + // field2 assert.strictEqual(entries[2].FIELDNAME, 'FIELD2'); - assert.strictEqual(entries[2].POSITION, '0003'); + assert.strictEqual(entries[2].POSITION, undefined); assert.strictEqual(entries.length, 3); }); @@ -656,24 +656,24 @@ describe('CDS-to-abapGit include directives', () => { const def = ast.definitions[0] as any; const entries = await buildDD03P(def.members); - // field1 at position 1 + // field1 assert.strictEqual(entries[0].FIELDNAME, 'FIELD1'); - assert.strictEqual(entries[0].POSITION, '0001'); + assert.strictEqual(entries[0].POSITION, undefined); - // .INCLUDE at position 2 (plain include) + // .INCLUDE (plain include) assert.strictEqual(entries[1].FIELDNAME, '.INCLUDE'); - assert.strictEqual(entries[1].POSITION, '0002'); + assert.strictEqual(entries[1].POSITION, undefined); assert.strictEqual(entries[1].PRECFIELD, 'ZAGE_STRUCTURE1'); - // .INCLU-_XX at position 3 (include with suffix _xx) + // .INCLU-_XX (include with suffix _xx) assert.strictEqual(entries[2].FIELDNAME, '.INCLU-_XX'); - assert.strictEqual(entries[2].POSITION, '0003'); + assert.strictEqual(entries[2].POSITION, undefined); assert.strictEqual(entries[2].PRECFIELD, 'ZAGE_STRUCTURE1'); assert.strictEqual(entries[2].COMPTYPE, 'S'); - // field2 at position 4 + // field2 assert.strictEqual(entries[3].FIELDNAME, 'FIELD2'); - assert.strictEqual(entries[3].POSITION, '0004'); + assert.strictEqual(entries[3].POSITION, undefined); assert.strictEqual(entries.length, 4); }); From b1357acfba02f74bb589fd69c3fc4fa40306e098 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 07:44:21 +0100 Subject: [PATCH 07/19] fix(adt-diff): use .acds extension for CDS DDL diff display SAP ABAP File Formats uses .acds for CDS source code files, not .ddl. Updated the diff display label accordingly. Generated with Devin Co-Authored-By: Devin --- packages/adt-diff/src/commands/diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adt-diff/src/commands/diff.ts b/packages/adt-diff/src/commands/diff.ts index 9054d02a..ee4a789a 100644 --- a/packages/adt-diff/src/commands/diff.ts +++ b/packages/adt-diff/src/commands/diff.ts @@ -377,7 +377,7 @@ export const diffCommand: CliCommandPlugin = { process.exit(1); } - const ddlFile = `${objectName}.tabl.ddl`; + const ddlFile = `${objectName}.tabl.acds`; const diffFound = printDiff( ddlFile, ddlFile, From ff711e8f9d81ec8888c3fdf70b52fd3891d29f2e Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 07:49:18 +0100 Subject: [PATCH 08/19] feat(cds-to-abapgit): detect LANGDEP from spras/lang fields in CDS LANGDEP=X is set by SAP DDIC when a structure/table contains a language-typed field (data element SPRAS or builtin type abap.lang). This is detectable from CDS source by checking if any member field references spras (named type) or uses abap.lang (builtin type). Fixes the roundtrip diff for zage_structure which has language : spras. Generated with Devin Co-Authored-By: Devin --- .../src/lib/handlers/cds-to-abapgit.ts | 18 +++++++++++++----- .../tests/handlers/tabl.test.ts | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index b521db99..5de33381 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -166,16 +166,24 @@ export function buildDD02V( 'AbapCatalog.enhancement.category', ); + // Detect language-dependent structure/table: + // LANGDEP=X when any field references data element SPRAS or builtin type abap.lang + const hasLanguageField = def.members.some((m) => { + if ('kind' in m && m.kind === 'include') return false; + const f = m as FieldDefinition; + if (f.type.kind === 'named') return f.type.name.toUpperCase() === 'SPRAS'; + if (f.type.kind === 'builtin') + return (f.type as BuiltinTypeRef).name === 'lang'; + return false; + }); + // Build result in standard abapGit DD02V field order: - // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS - // - // NOTE: LANGDEP and CLIDEP are real DD02V database values that cannot be - // reliably determined from CDS source alone. abapGit reads them from SAP - // and only emits them when set. We omit them here to avoid roundtrip mismatches. + // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS const result: DD02VData = {}; result.TABNAME = def.name.toUpperCase(); result.DDLANGUAGE = language; result.TABCLASS = tabclass; + if (hasLanguageField) result.LANGDEP = 'X'; result.DDTEXT = description; result.MASTERLANG = language; if (deliveryClass) result.CONTFLAG = deliveryClass; diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts index 401b54f9..b77e2de9 100644 --- a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -619,7 +619,21 @@ describe('CDS-to-abapGit field type mapping (all builtin types)', () => { }); describe('CDS-to-abapGit DD02V LANGDEP', () => { - it('buildDD02V does not emit LANGDEP (cannot determine from CDS)', () => { + it('buildDD02V sets LANGDEP=X when structure has spras field', () => { + const { ast } = parse(CDS_ALL_TYPES); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'Simple structure'); + assert.strictEqual(dd02v.LANGDEP, 'X'); + }); + + it('buildDD02V omits LANGDEP when no language field present', () => { + const { ast } = parse(CDS_TRANSPARENT_TABLE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'Test Table'); + assert.strictEqual(dd02v.LANGDEP, undefined); + }); + + it('buildDD02V omits LANGDEP for simple structure without spras', () => { const { ast } = parse(CDS_STRUCTURE); const def = ast.definitions[0] as any; const dd02v = buildDD02V(def, 'E', 'AGE Test Structure'); From 20a4ac77f421a722bea416a26c21643ed9148e2b Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 07:53:34 +0100 Subject: [PATCH 09/19] fix(cds-to-abapgit): correct DD03P field ordering to match SAP Reorder field assignments in buildFieldDD03P to match the canonical DD03P structure definition order used by SAP/abapGit: FIELDNAME, KEYFLAG, ROLLNAME, ADMINFIELD, INTTYPE, INTLEN, NOTNULL, DATATYPE, LENG, DECIMALS, SHLPORIGIN, MASK, COMPTYPE, REFTABLE, REFFIELD Previously REFTABLE/REFFIELD were emitted before DATATYPE, and MASK was emitted after REFTABLE/REFFIELD. This caused field ordering diffs in roundtrip comparison even though the data was identical. Generated with Devin Co-Authored-By: Devin --- .../src/lib/handlers/cds-to-abapgit.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index 5de33381..12bcefb3 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -385,11 +385,9 @@ async function buildFieldDD03P( } } - // Build in standard abapGit DD03P field order - // - // NOTE: POSITION is a real DD03P database value. abapGit emits it - // inconsistently (only for some transparent tables). We omit it to - // avoid roundtrip mismatches — SAP auto-computes it on import. + // Build in standard abapGit DD03P field order (matches DD03P structure definition): + // FIELDNAME, KEYFLAG, ROLLNAME, ADMINFIELD, INTTYPE, INTLEN, NOTNULL, + // DATATYPE, LENG, DECIMALS, SHLPORIGIN, MASK, COMPTYPE, REFTABLE, REFFIELD const result: DD03PData = {}; result.FIELDNAME = field.name.toUpperCase(); if (isKey) result.KEYFLAG = 'X'; @@ -402,10 +400,10 @@ async function buildFieldDD03P( if (leng) result.LENG = leng; if (decimals) result.DECIMALS = decimals; if (shlporigin) result.SHLPORIGIN = shlporigin; - if (reftable) result.REFTABLE = reftable; - if (reffield) result.REFFIELD = reffield; if (mask) result.MASK = mask; if (comptype) result.COMPTYPE = comptype; + if (reftable) result.REFTABLE = reftable; + if (reffield) result.REFFIELD = reffield; return result; } From 61ce2ba5dc25115161623a0fa31ac6d9d6fca9e9 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 07:54:05 +0100 Subject: [PATCH 10/19] Revert "fix(cds-to-abapgit): correct DD03P field ordering to match SAP" This reverts commit 20a4ac77f421a722bea416a26c21643ed9148e2b. --- .../src/lib/handlers/cds-to-abapgit.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index 12bcefb3..5de33381 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -385,9 +385,11 @@ async function buildFieldDD03P( } } - // Build in standard abapGit DD03P field order (matches DD03P structure definition): - // FIELDNAME, KEYFLAG, ROLLNAME, ADMINFIELD, INTTYPE, INTLEN, NOTNULL, - // DATATYPE, LENG, DECIMALS, SHLPORIGIN, MASK, COMPTYPE, REFTABLE, REFFIELD + // Build in standard abapGit DD03P field order + // + // NOTE: POSITION is a real DD03P database value. abapGit emits it + // inconsistently (only for some transparent tables). We omit it to + // avoid roundtrip mismatches — SAP auto-computes it on import. const result: DD03PData = {}; result.FIELDNAME = field.name.toUpperCase(); if (isKey) result.KEYFLAG = 'X'; @@ -400,10 +402,10 @@ async function buildFieldDD03P( if (leng) result.LENG = leng; if (decimals) result.DECIMALS = decimals; if (shlporigin) result.SHLPORIGIN = shlporigin; - if (mask) result.MASK = mask; - if (comptype) result.COMPTYPE = comptype; if (reftable) result.REFTABLE = reftable; if (reffield) result.REFFIELD = reffield; + if (mask) result.MASK = mask; + if (comptype) result.COMPTYPE = comptype; return result; } From d7578d36ff07ee5a4cb80aca0590d6e39385f6d6 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 08:33:39 +0100 Subject: [PATCH 11/19] fix(abapgit): align dd02v and dd03p field order Switch DD02V and DD03P XSD definitions from all to sequence so field order is preserved for TABL serialization and roundtrip compatibility with SAP/abapGit output. Regenerate the derived schema and type files to match the updated XSD definitions and ignore generated schema output in Prettier. --- .prettierignore | 3 +- .../src/schemas/generated/index.ts | 56 +--- .../src/schemas/generated/schemas/clas.ts | 168 +++++----- .../src/schemas/generated/schemas/devc.ts | 66 ++-- .../src/schemas/generated/schemas/doma.ts | 192 +++++------ .../src/schemas/generated/schemas/dtel.ts | 180 +++++------ .../src/schemas/generated/schemas/fugr.ts | 186 +++++------ .../src/schemas/generated/schemas/index.ts | 2 +- .../src/schemas/generated/schemas/intf.ts | 102 +++--- .../src/schemas/generated/schemas/prog.ts | 160 ++++----- .../src/schemas/generated/schemas/tabl.ts | 304 +++++++++--------- .../src/schemas/generated/schemas/ttyp.ts | 144 ++++----- .../src/schemas/generated/types/clas.ts | 109 +++---- .../src/schemas/generated/types/devc.ts | 39 ++- .../src/schemas/generated/types/doma.ts | 145 ++++----- .../src/schemas/generated/types/dtel.ts | 117 ++++--- .../src/schemas/generated/types/fugr.ts | 74 ++--- .../src/schemas/generated/types/index.ts | 2 +- .../src/schemas/generated/types/intf.ts | 65 ++-- .../src/schemas/generated/types/prog.ts | 52 +-- .../src/schemas/generated/types/tabl.ts | 249 +++++++------- .../src/schemas/generated/types/ttyp.ts | 93 +++--- .../adt-plugin-abapgit/xsd/types/dd02v.xsd | 4 +- .../adt-plugin-abapgit/xsd/types/dd03p.xsd | 22 +- 24 files changed, 1241 insertions(+), 1293 deletions(-) diff --git a/.prettierignore b/.prettierignore index 89d58b42..732b2a91 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,5 @@ /coverage /.nx/cache /.nx/workspace-data -/git_modules \ No newline at end of file +/git_modules +**/schemas/generated \ No newline at end of file diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/index.ts b/packages/adt-plugin-abapgit/src/schemas/generated/index.ts index bc52f1c4..dac3b132 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/index.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/index.ts @@ -1,8 +1,8 @@ /** * AbapGit Typed Schemas - * + * * Auto-generated by ts-xsd codegen - DO NOT EDIT - * + * * Each schema provides: * - _type: Full AbapGitType (for XML build/parse) * - _values: Inner values type (for handler mapping) @@ -48,47 +48,15 @@ type TablAbapGitType = Extract<_TablSchema, { abapGit: unknown }>; type TtypAbapGitType = Extract<_TtypSchema, { abapGit: unknown }>; // AbapGit schema instances - using flattened types with values extracted from abapGit.abap.values -export const clas = abapGitSchema< - ClasAbapGitType, - ClasAbapGitType['abapGit']['abap']['values'] ->(_clas); -export const devc = abapGitSchema< - DevcAbapGitType, - DevcAbapGitType['abapGit']['abap']['values'] ->(_devc); -export const doma = abapGitSchema< - DomaAbapGitType, - DomaAbapGitType['abapGit']['abap']['values'] ->(_doma); -export const dtel = abapGitSchema< - DtelAbapGitType, - DtelAbapGitType['abapGit']['abap']['values'] ->(_dtel); -export const intf = abapGitSchema< - IntfAbapGitType, - IntfAbapGitType['abapGit']['abap']['values'] ->(_intf); -export const prog = abapGitSchema< - ProgAbapGitType, - ProgAbapGitType['abapGit']['abap']['values'] ->(_prog); -export const fugr = abapGitSchema< - FugrAbapGitType, - FugrAbapGitType['abapGit']['abap']['values'] ->(_fugr); -export const tabl = abapGitSchema< - TablAbapGitType, - TablAbapGitType['abapGit']['abap']['values'] ->(_tabl); -export const ttyp = abapGitSchema< - TtypAbapGitType, - TtypAbapGitType['abapGit']['abap']['values'] ->(_ttyp); +export const clas = abapGitSchema(_clas); +export const devc = abapGitSchema(_devc); +export const doma = abapGitSchema(_doma); +export const dtel = abapGitSchema(_dtel); +export const intf = abapGitSchema(_intf); +export const prog = abapGitSchema(_prog); +export const fugr = abapGitSchema(_fugr); +export const tabl = abapGitSchema(_tabl); +export const ttyp = abapGitSchema(_ttyp); // Re-export types and utilities -export { - abapGitSchema, - type AbapGitSchema, - type InferAbapGitType, - type InferValuesType, -} from '../../lib/handlers/abapgit-schema'; +export { abapGitSchema, type AbapGitSchema, type InferAbapGitType, type InferValuesType } from '../../lib/handlers/abapgit-schema'; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts index fe97c69e..7e7b9966 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/clas.ts @@ -1,183 +1,183 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/clas.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'VSEOCLASS', - type: 'asx:VseoClassType', - minOccurs: '0', + name: "VSEOCLASS", + type: "asx:VseoClassType", + minOccurs: "0", }, ], }, }, { - name: 'VseoClassType', + name: "VseoClassType", all: { element: [ { - name: 'CLSNAME', - type: 'xs:string', + name: "CLSNAME", + type: "xs:string", }, { - name: 'LANGU', - type: 'xs:string', - minOccurs: '0', + name: "LANGU", + type: "xs:string", + minOccurs: "0", }, { - name: 'DESCRIPT', - type: 'xs:string', - minOccurs: '0', + name: "DESCRIPT", + type: "xs:string", + minOccurs: "0", }, { - name: 'STATE', - type: 'xs:string', - minOccurs: '0', + name: "STATE", + type: "xs:string", + minOccurs: "0", }, { - name: 'CATEGORY', - type: 'xs:string', - minOccurs: '0', + name: "CATEGORY", + type: "xs:string", + minOccurs: "0", }, { - name: 'EXPOSURE', - type: 'xs:string', - minOccurs: '0', + name: "EXPOSURE", + type: "xs:string", + minOccurs: "0", }, { - name: 'CLSFINAL', - type: 'xs:string', - minOccurs: '0', + name: "CLSFINAL", + type: "xs:string", + minOccurs: "0", }, { - name: 'CLSABSTRCT', - type: 'xs:string', - minOccurs: '0', + name: "CLSABSTRCT", + type: "xs:string", + minOccurs: "0", }, { - name: 'CLSCCINCL', - type: 'xs:string', - minOccurs: '0', + name: "CLSCCINCL", + type: "xs:string", + minOccurs: "0", }, { - name: 'FIXPT', - type: 'xs:string', - minOccurs: '0', + name: "FIXPT", + type: "xs:string", + minOccurs: "0", }, { - name: 'UNICODE', - type: 'xs:string', - minOccurs: '0', + name: "UNICODE", + type: "xs:string", + minOccurs: "0", }, { - name: 'WITH_UNIT_TESTS', - type: 'xs:string', - minOccurs: '0', + name: "WITH_UNIT_TESTS", + type: "xs:string", + minOccurs: "0", }, { - name: 'DURATION', - type: 'xs:string', - minOccurs: '0', + name: "DURATION", + type: "xs:string", + minOccurs: "0", }, { - name: 'RISK', - type: 'xs:string', - minOccurs: '0', + name: "RISK", + type: "xs:string", + minOccurs: "0", }, { - name: 'MSG_ID', - type: 'xs:string', - minOccurs: '0', + name: "MSG_ID", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFCLSNAME', - type: 'xs:string', - minOccurs: '0', + name: "REFCLSNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'SHRM_ENABLED', - type: 'xs:string', - minOccurs: '0', + name: "SHRM_ENABLED", + type: "xs:string", + minOccurs: "0", }, { - name: 'ABAP_LANGUAGE_VERSION', - type: 'xs:string', - minOccurs: '0', + name: "ABAP_LANGUAGE_VERSION", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts index b68690e0..2cbb0a2b 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/devc.ts @@ -1,98 +1,98 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/devc.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'DEVC', - type: 'asx:DevcType', - minOccurs: '0', + name: "DEVC", + type: "asx:DevcType", + minOccurs: "0", }, ], }, }, { - name: 'DevcType', + name: "DevcType", all: { element: [ { - name: 'CTEXT', - type: 'xs:string', + name: "CTEXT", + type: "xs:string", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts index 20c3c0b7..8734a18f 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/doma.ts @@ -1,213 +1,213 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/doma.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'DD01V', - type: 'asx:Dd01vType', - minOccurs: '0', + name: "DD01V", + type: "asx:Dd01vType", + minOccurs: "0", }, { - name: 'DD07V_TAB', - type: 'asx:Dd07vTabType', - minOccurs: '0', + name: "DD07V_TAB", + type: "asx:Dd07vTabType", + minOccurs: "0", }, ], }, }, { - name: 'Dd01vType', + name: "Dd01vType", all: { element: [ { - name: 'DOMNAME', - type: 'xs:string', + name: "DOMNAME", + type: "xs:string", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DATATYPE', - type: 'xs:string', - minOccurs: '0', + name: "DATATYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'LENG', - type: 'xs:string', - minOccurs: '0', + name: "LENG", + type: "xs:string", + minOccurs: "0", }, { - name: 'OUTPUTLEN', - type: 'xs:string', - minOccurs: '0', + name: "OUTPUTLEN", + type: "xs:string", + minOccurs: "0", }, { - name: 'DECIMALS', - type: 'xs:string', - minOccurs: '0', + name: "DECIMALS", + type: "xs:string", + minOccurs: "0", }, { - name: 'LOWERCASE', - type: 'xs:string', - minOccurs: '0', + name: "LOWERCASE", + type: "xs:string", + minOccurs: "0", }, { - name: 'SIGNFLAG', - type: 'xs:string', - minOccurs: '0', + name: "SIGNFLAG", + type: "xs:string", + minOccurs: "0", }, { - name: 'VALEXI', - type: 'xs:string', - minOccurs: '0', + name: "VALEXI", + type: "xs:string", + minOccurs: "0", }, { - name: 'ENTITYTAB', - type: 'xs:string', - minOccurs: '0', + name: "ENTITYTAB", + type: "xs:string", + minOccurs: "0", }, { - name: 'CONVEXIT', - type: 'xs:string', - minOccurs: '0', + name: "CONVEXIT", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'DOMMASTER', - type: 'xs:string', - minOccurs: '0', + name: "DOMMASTER", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'Dd07vType', + name: "Dd07vType", all: { element: [ { - name: 'DOMNAME', - type: 'xs:string', - minOccurs: '0', + name: "DOMNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'VALPOS', - type: 'xs:string', - minOccurs: '0', + name: "VALPOS", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DOMVALUE_L', - type: 'xs:string', - minOccurs: '0', + name: "DOMVALUE_L", + type: "xs:string", + minOccurs: "0", }, { - name: 'DOMVALUE_H', - type: 'xs:string', - minOccurs: '0', + name: "DOMVALUE_H", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'Dd07vTabType', + name: "Dd07vTabType", sequence: { element: [ { - name: 'DD07V', - type: 'Dd07vType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "DD07V", + type: "Dd07vType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts index 7c2dc9e6..b3b95647 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/dtel.ts @@ -1,193 +1,193 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/dtel.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'DD04V', - type: 'asx:Dd04vType', - minOccurs: '0', + name: "DD04V", + type: "asx:Dd04vType", + minOccurs: "0", }, ], }, }, { - name: 'Dd04vType', + name: "Dd04vType", sequence: { element: [ { - name: 'ROLLNAME', - type: 'xs:string', + name: "ROLLNAME", + type: "xs:string", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DOMNAME', - type: 'xs:string', - minOccurs: '0', + name: "DOMNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'HEADLEN', - type: 'xs:string', - minOccurs: '0', + name: "HEADLEN", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRLEN1', - type: 'xs:string', - minOccurs: '0', + name: "SCRLEN1", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRLEN2', - type: 'xs:string', - minOccurs: '0', + name: "SCRLEN2", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRLEN3', - type: 'xs:string', - minOccurs: '0', + name: "SCRLEN3", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'REPTEXT', - type: 'xs:string', - minOccurs: '0', + name: "REPTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRTEXT_S', - type: 'xs:string', - minOccurs: '0', + name: "SCRTEXT_S", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRTEXT_M', - type: 'xs:string', - minOccurs: '0', + name: "SCRTEXT_M", + type: "xs:string", + minOccurs: "0", }, { - name: 'SCRTEXT_L', - type: 'xs:string', - minOccurs: '0', + name: "SCRTEXT_L", + type: "xs:string", + minOccurs: "0", }, { - name: 'DTELMASTER', - type: 'xs:string', - minOccurs: '0', + name: "DTELMASTER", + type: "xs:string", + minOccurs: "0", }, { - name: 'DATATYPE', - type: 'xs:string', - minOccurs: '0', + name: "DATATYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'LENG', - type: 'xs:string', - minOccurs: '0', + name: "LENG", + type: "xs:string", + minOccurs: "0", }, { - name: 'DECIMALS', - type: 'xs:string', - minOccurs: '0', + name: "DECIMALS", + type: "xs:string", + minOccurs: "0", }, { - name: 'OUTPUTLEN', - type: 'xs:string', - minOccurs: '0', + name: "OUTPUTLEN", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFKIND', - type: 'xs:string', - minOccurs: '0', + name: "REFKIND", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFTYPE', - type: 'xs:string', - minOccurs: '0', + name: "REFTYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ABAP_LANGUAGE_VERSION', - type: 'xs:string', - minOccurs: '0', + name: "ABAP_LANGUAGE_VERSION", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts index e2daeb08..8792ac76 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/fugr.ts @@ -1,233 +1,233 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/fugr.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - elementFormDefault: 'unqualified', + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - name: 'abap', - type: 'FugrAbapType', + name: "abap", + type: "FugrAbapType", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, ], complexType: [ { - name: 'FugrValuesType', + name: "FugrValuesType", all: { element: [ { - name: 'AREAT', - type: 'xs:string', - minOccurs: '0', + name: "AREAT", + type: "xs:string", + minOccurs: "0", }, { - name: 'INCLUDES', - type: 'FugrIncludesType', - minOccurs: '0', + name: "INCLUDES", + type: "FugrIncludesType", + minOccurs: "0", }, { - name: 'FUNCTIONS', - type: 'FugrFunctionsType', - minOccurs: '0', + name: "FUNCTIONS", + type: "FugrFunctionsType", + minOccurs: "0", }, ], }, }, { - name: 'FugrAbapType', + name: "FugrAbapType", sequence: { element: [ { - name: 'values', - type: 'FugrValuesType', + name: "values", + type: "FugrValuesType", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, { - name: 'AbapValuesType', + name: "AbapValuesType", sequence: { element: [ { - ref: 'asx:Schema', - minOccurs: '0', - maxOccurs: 'unbounded', + ref: "asx:Schema", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, { - name: 'FugrParamType', + name: "FugrParamType", all: { element: [ { - name: 'PARAMETER', - type: 'xs:string', - minOccurs: '0', + name: "PARAMETER", + type: "xs:string", + minOccurs: "0", }, { - name: 'TYP', - type: 'xs:string', - minOccurs: '0', + name: "TYP", + type: "xs:string", + minOccurs: "0", }, { - name: 'DBFIELD', - type: 'xs:string', - minOccurs: '0', + name: "DBFIELD", + type: "xs:string", + minOccurs: "0", }, { - name: 'DEFAULT', - type: 'xs:string', - minOccurs: '0', + name: "DEFAULT", + type: "xs:string", + minOccurs: "0", }, { - name: 'OPTIONAL', - type: 'xs:string', - minOccurs: '0', + name: "OPTIONAL", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFERENCE', - type: 'xs:string', - minOccurs: '0', + name: "REFERENCE", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'FugrImportType', + name: "FugrImportType", sequence: { element: [ { - name: 'RSIMP', - type: 'FugrParamType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "RSIMP", + type: "FugrParamType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'FugrExportType', + name: "FugrExportType", sequence: { element: [ { - name: 'RSEXP', - type: 'FugrParamType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "RSEXP", + type: "FugrParamType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'FugrFunctionItemType', + name: "FugrFunctionItemType", all: { element: [ { - name: 'FUNCNAME', - type: 'xs:string', + name: "FUNCNAME", + type: "xs:string", }, { - name: 'SHORT_TEXT', - type: 'xs:string', - minOccurs: '0', + name: "SHORT_TEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'IMPORT', - type: 'FugrImportType', - minOccurs: '0', + name: "IMPORT", + type: "FugrImportType", + minOccurs: "0", }, { - name: 'EXPORT', - type: 'FugrExportType', - minOccurs: '0', + name: "EXPORT", + type: "FugrExportType", + minOccurs: "0", }, ], }, }, { - name: 'FugrFunctionsType', + name: "FugrFunctionsType", sequence: { element: [ { - name: 'item', - type: 'FugrFunctionItemType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "item", + type: "FugrFunctionItemType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'FugrIncludesType', + name: "FugrIncludesType", sequence: { element: [ { - name: 'SOBJ_NAME', - type: 'xs:string', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "SOBJ_NAME", + type: "xs:string", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/index.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/index.ts index 8eb4c998..db97e1c5 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/index.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/index.ts @@ -1,6 +1,6 @@ /** * Auto-generated index for abapgit schemas - * + * * DO NOT EDIT - Generated by ts-xsd codegen */ diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts index aa7f12eb..65e94200 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/intf.ts @@ -1,128 +1,128 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/intf.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'VSEOINTERF', - type: 'asx:VseoInterfType', - minOccurs: '0', + name: "VSEOINTERF", + type: "asx:VseoInterfType", + minOccurs: "0", }, ], }, }, { - name: 'VseoInterfType', + name: "VseoInterfType", all: { element: [ { - name: 'CLSNAME', - type: 'xs:string', + name: "CLSNAME", + type: "xs:string", }, { - name: 'LANGU', - type: 'xs:string', - minOccurs: '0', + name: "LANGU", + type: "xs:string", + minOccurs: "0", }, { - name: 'DESCRIPT', - type: 'xs:string', - minOccurs: '0', + name: "DESCRIPT", + type: "xs:string", + minOccurs: "0", }, { - name: 'EXPOSURE', - type: 'xs:string', - minOccurs: '0', + name: "EXPOSURE", + type: "xs:string", + minOccurs: "0", }, { - name: 'STATE', - type: 'xs:string', - minOccurs: '0', + name: "STATE", + type: "xs:string", + minOccurs: "0", }, { - name: 'UNICODE', - type: 'xs:string', - minOccurs: '0', + name: "UNICODE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ABAP_LANGUAGE_VERSION', - type: 'xs:string', - minOccurs: '0', + name: "ABAP_LANGUAGE_VERSION", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts index 847c9e51..d483fb2a 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/prog.ts @@ -1,198 +1,198 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/prog.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - elementFormDefault: 'unqualified', + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - name: 'abap', - type: 'ProgAbapType', + name: "abap", + type: "ProgAbapType", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, ], complexType: [ { - name: 'ProgValuesType', + name: "ProgValuesType", all: { element: [ { - name: 'PROGDIR', - type: 'ProgdirType', - minOccurs: '0', + name: "PROGDIR", + type: "ProgdirType", + minOccurs: "0", }, { - name: 'TPOOL', - type: 'TpoolType', - minOccurs: '0', + name: "TPOOL", + type: "TpoolType", + minOccurs: "0", }, ], }, }, { - name: 'ProgAbapType', + name: "ProgAbapType", sequence: { element: [ { - name: 'values', - type: 'ProgValuesType', + name: "values", + type: "ProgValuesType", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, { - name: 'AbapValuesType', + name: "AbapValuesType", sequence: { element: [ { - ref: 'asx:Schema', - minOccurs: '0', - maxOccurs: 'unbounded', + ref: "asx:Schema", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, { - name: 'ProgdirType', + name: "ProgdirType", all: { element: [ { - name: 'NAME', - type: 'xs:string', + name: "NAME", + type: "xs:string", }, { - name: 'STATE', - type: 'xs:string', - minOccurs: '0', + name: "STATE", + type: "xs:string", + minOccurs: "0", }, { - name: 'SUBC', - type: 'xs:string', - minOccurs: '0', + name: "SUBC", + type: "xs:string", + minOccurs: "0", }, { - name: 'FIXPT', - type: 'xs:string', - minOccurs: '0', + name: "FIXPT", + type: "xs:string", + minOccurs: "0", }, { - name: 'UNICODE', - type: 'xs:string', - minOccurs: '0', + name: "UNICODE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DTEFUNC', - type: 'xs:string', - minOccurs: '0', + name: "DTEFUNC", + type: "xs:string", + minOccurs: "0", }, { - name: 'RLOAD', - type: 'xs:string', - minOccurs: '0', + name: "RLOAD", + type: "xs:string", + minOccurs: "0", }, { - name: 'UCCHECK', - type: 'xs:string', - minOccurs: '0', + name: "UCCHECK", + type: "xs:string", + minOccurs: "0", }, { - name: 'ABAP_LANGUAGE_VERSION', - type: 'xs:string', - minOccurs: '0', + name: "ABAP_LANGUAGE_VERSION", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'TpoolItemType', + name: "TpoolItemType", all: { element: [ { - name: 'ID', - type: 'xs:string', + name: "ID", + type: "xs:string", }, { - name: 'ENTRY', - type: 'xs:string', - minOccurs: '0', + name: "ENTRY", + type: "xs:string", + minOccurs: "0", }, { - name: 'LENGTH', - type: 'xs:string', - minOccurs: '0', + name: "LENGTH", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'TpoolType', + name: "TpoolType", sequence: { element: [ { - name: 'item', - type: 'TpoolItemType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "item", + type: "TpoolItemType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts index 1edf76a5..9de0fdd7 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/tabl.ts @@ -1,303 +1,303 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/tabl.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'DD02V', - type: 'asx:Dd02vType', - minOccurs: '0', + name: "DD02V", + type: "asx:Dd02vType", + minOccurs: "0", }, { - name: 'DD03P_TABLE', - type: 'asx:Dd03pTableType', - minOccurs: '0', + name: "DD03P_TABLE", + type: "asx:Dd03pTableType", + minOccurs: "0", }, ], }, }, { - name: 'Dd02vType', - all: { + name: "Dd02vType", + sequence: { element: [ { - name: 'TABNAME', - type: 'xs:string', + name: "TABNAME", + type: "xs:string", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'TABCLASS', - type: 'xs:string', - minOccurs: '0', + name: "TABCLASS", + type: "xs:string", + minOccurs: "0", }, { - name: 'LANGDEP', - type: 'xs:string', - minOccurs: '0', + name: "LANGDEP", + type: "xs:string", + minOccurs: "0", }, { - name: 'CLIDEP', - type: 'xs:string', - minOccurs: '0', + name: "CLIDEP", + type: "xs:string", + minOccurs: "0", }, { - name: 'SQLTAB', - type: 'xs:string', - minOccurs: '0', + name: "SQLTAB", + type: "xs:string", + minOccurs: "0", }, { - name: 'DATCLASS', - type: 'xs:string', - minOccurs: '0', + name: "DATCLASS", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'MASTERLANG', - type: 'xs:string', - minOccurs: '0', + name: "MASTERLANG", + type: "xs:string", + minOccurs: "0", }, { - name: 'BUFFERED', - type: 'xs:string', - minOccurs: '0', + name: "BUFFERED", + type: "xs:string", + minOccurs: "0", }, { - name: 'MATEFLAG', - type: 'xs:string', - minOccurs: '0', + name: "MATEFLAG", + type: "xs:string", + minOccurs: "0", }, { - name: 'CONTFLAG', - type: 'xs:string', - minOccurs: '0', + name: "CONTFLAG", + type: "xs:string", + minOccurs: "0", }, { - name: 'SHLPEXI', - type: 'xs:string', - minOccurs: '0', + name: "SHLPEXI", + type: "xs:string", + minOccurs: "0", }, { - name: 'EXCLASS', - type: 'xs:string', - minOccurs: '0', + name: "EXCLASS", + type: "xs:string", + minOccurs: "0", }, { - name: 'AUTHCLASS', - type: 'xs:string', - minOccurs: '0', + name: "AUTHCLASS", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'Dd03pType', - all: { + name: "Dd03pType", + sequence: { element: [ { - name: 'TABNAME', - type: 'xs:string', - minOccurs: '0', + name: "FIELDNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'FIELDNAME', - type: 'xs:string', - minOccurs: '0', + name: "POSITION", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "KEYFLAG", + type: "xs:string", + minOccurs: "0", }, { - name: 'POSITION', - type: 'xs:string', - minOccurs: '0', + name: "ROLLNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'KEYFLAG', - type: 'xs:string', - minOccurs: '0', + name: "ADMINFIELD", + type: "xs:string", + minOccurs: "0", }, { - name: 'ROLLNAME', - type: 'xs:string', - minOccurs: '0', + name: "INTTYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ADMINFIELD', - type: 'xs:string', - minOccurs: '0', + name: "INTLEN", + type: "xs:string", + minOccurs: "0", }, { - name: 'INTTYPE', - type: 'xs:string', - minOccurs: '0', + name: "REFTABLE", + type: "xs:string", + minOccurs: "0", }, { - name: 'INTLEN', - type: 'xs:string', - minOccurs: '0', + name: "REFFIELD", + type: "xs:string", + minOccurs: "0", }, { - name: 'NOTNULL', - type: 'xs:string', - minOccurs: '0', + name: "DATATYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DATATYPE', - type: 'xs:string', - minOccurs: '0', + name: "LENG", + type: "xs:string", + minOccurs: "0", }, { - name: 'LENG', - type: 'xs:string', - minOccurs: '0', + name: "DECIMALS", + type: "xs:string", + minOccurs: "0", }, { - name: 'DECIMALS', - type: 'xs:string', - minOccurs: '0', + name: "NOTNULL", + type: "xs:string", + minOccurs: "0", }, { - name: 'DOMNAME', - type: 'xs:string', - minOccurs: '0', + name: "DOMNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'SHLPORIGIN', - type: 'xs:string', - minOccurs: '0', + name: "PRECFIELD", + type: "xs:string", + minOccurs: "0", }, { - name: 'MASK', - type: 'xs:string', - minOccurs: '0', + name: "MASK", + type: "xs:string", + minOccurs: "0", }, { - name: 'COMPTYPE', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFTABLE', - type: 'xs:string', - minOccurs: '0', + name: "SHLPORIGIN", + type: "xs:string", + minOccurs: "0", }, { - name: 'REFFIELD', - type: 'xs:string', - minOccurs: '0', + name: "COMPTYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'CONRFLAG', - type: 'xs:string', - minOccurs: '0', + name: "TABNAME", + type: "xs:string", + minOccurs: "0", }, { - name: 'PRECFIELD', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "CONRFLAG", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'Dd03pTableType', + name: "Dd03pTableType", sequence: { element: [ { - name: 'DD03P', - type: 'Dd03pType', - minOccurs: '0', - maxOccurs: 'unbounded', + name: "DD03P", + type: "Dd03pType", + minOccurs: "0", + maxOccurs: "unbounded", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts index 37470369..259a9c6e 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/schemas/ttyp.ts @@ -1,163 +1,163 @@ /** * Auto-generated schema from XSD - * + * * DO NOT EDIT - Generated by ts-xsd codegen * Source: abapgit/ttyp.xsd */ export default { $xmlns: { - xs: 'http://www.w3.org/2001/XMLSchema', - asx: 'http://www.sap.com/abapxml', + xs: "http://www.w3.org/2001/XMLSchema", + asx: "http://www.sap.com/abapxml", }, - targetNamespace: 'http://www.sap.com/abapxml', - elementFormDefault: 'unqualified', + targetNamespace: "http://www.sap.com/abapxml", + elementFormDefault: "unqualified", element: [ { - name: 'abapGit', + name: "abapGit", complexType: { sequence: { element: [ { - ref: 'asx:abap', + ref: "asx:abap", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - use: 'required', + name: "version", + type: "xs:string", + use: "required", }, { - name: 'serializer', - type: 'xs:string', - use: 'required', + name: "serializer", + type: "xs:string", + use: "required", }, { - name: 'serializer_version', - type: 'xs:string', - use: 'required', + name: "serializer_version", + type: "xs:string", + use: "required", }, ], }, }, { - name: 'Schema', + name: "Schema", abstract: true, }, { - name: 'values', - type: 'asx:AbapValuesType', + name: "values", + type: "asx:AbapValuesType", }, { - name: 'abap', - type: 'asx:AbapType', + name: "abap", + type: "asx:AbapType", }, ], complexType: [ { - name: 'AbapValuesType', + name: "AbapValuesType", all: { element: [ { - name: 'DD40V', - type: 'asx:Dd40vType', - minOccurs: '0', + name: "DD40V", + type: "asx:Dd40vType", + minOccurs: "0", }, ], }, }, { - name: 'Dd40vType', + name: "Dd40vType", all: { element: [ { - name: 'TYPENAME', - type: 'xs:string', + name: "TYPENAME", + type: "xs:string", }, { - name: 'DDLANGUAGE', - type: 'xs:string', - minOccurs: '0', + name: "DDLANGUAGE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ROWTYPE', - type: 'xs:string', - minOccurs: '0', + name: "ROWTYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ROWKIND', - type: 'xs:string', - minOccurs: '0', + name: "ROWKIND", + type: "xs:string", + minOccurs: "0", }, { - name: 'DATATYPE', - type: 'xs:string', - minOccurs: '0', + name: "DATATYPE", + type: "xs:string", + minOccurs: "0", }, { - name: 'ACCESSMODE', - type: 'xs:string', - minOccurs: '0', + name: "ACCESSMODE", + type: "xs:string", + minOccurs: "0", }, { - name: 'KEYDEF', - type: 'xs:string', - minOccurs: '0', + name: "KEYDEF", + type: "xs:string", + minOccurs: "0", }, { - name: 'KEYKIND', - type: 'xs:string', - minOccurs: '0', + name: "KEYKIND", + type: "xs:string", + minOccurs: "0", }, { - name: 'GENERIC', - type: 'xs:string', - minOccurs: '0', + name: "GENERIC", + type: "xs:string", + minOccurs: "0", }, { - name: 'LENG', - type: 'xs:string', - minOccurs: '0', + name: "LENG", + type: "xs:string", + minOccurs: "0", }, { - name: 'DECIMALS', - type: 'xs:string', - minOccurs: '0', + name: "DECIMALS", + type: "xs:string", + minOccurs: "0", }, { - name: 'DDTEXT', - type: 'xs:string', - minOccurs: '0', + name: "DDTEXT", + type: "xs:string", + minOccurs: "0", }, { - name: 'TYPELEN', - type: 'xs:string', - minOccurs: '0', + name: "TYPELEN", + type: "xs:string", + minOccurs: "0", }, { - name: 'DEFFDNAME', - type: 'xs:string', - minOccurs: '0', + name: "DEFFDNAME", + type: "xs:string", + minOccurs: "0", }, ], }, }, { - name: 'AbapType', + name: "AbapType", sequence: { element: [ { - ref: 'asx:values', + ref: "asx:values", }, ], }, attribute: [ { - name: 'version', - type: 'xs:string', - default: '1.0', + name: "version", + type: "xs:string", + "default": "1.0", }, ], }, diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts index 5bf63c7e..0d915910 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/clas.ts @@ -5,67 +5,40 @@ * Mode: Flattened */ -export type ClasSchema = - | { - abapGit: { +export type ClasSchema = { + abapGit: { abap: { - values: { - VSEOCLASS?: { - CLSNAME: string; - LANGU?: string; - DESCRIPT?: string; - STATE?: string; - CATEGORY?: string; - EXPOSURE?: string; - CLSFINAL?: string; - CLSABSTRCT?: string; - CLSCCINCL?: string; - FIXPT?: string; - UNICODE?: string; - WITH_UNIT_TESTS?: string; - DURATION?: string; - RISK?: string; - MSG_ID?: string; - REFCLSNAME?: string; - SHRM_ENABLED?: string; - ABAP_LANGUAGE_VERSION?: string; + values: { + VSEOCLASS?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + STATE?: string; + CATEGORY?: string; + EXPOSURE?: string; + CLSFINAL?: string; + CLSABSTRCT?: string; + CLSCCINCL?: string; + FIXPT?: string; + UNICODE?: string; + WITH_UNIT_TESTS?: string; + DURATION?: string; + RISK?: string; + MSG_ID?: string; + REFCLSNAME?: string; + SHRM_ENABLED?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { VSEOCLASS?: { - CLSNAME: string; - LANGU?: string; - DESCRIPT?: string; - STATE?: string; - CATEGORY?: string; - EXPOSURE?: string; - CLSFINAL?: string; - CLSABSTRCT?: string; - CLSCCINCL?: string; - FIXPT?: string; - UNICODE?: string; - WITH_UNIT_TESTS?: string; - DURATION?: string; - RISK?: string; - MSG_ID?: string; - REFCLSNAME?: string; - SHRM_ENABLED?: string; - ABAP_LANGUAGE_VERSION?: string; - }; - }; - } - | { - abap: { - values: { - VSEOCLASS?: { CLSNAME: string; LANGU?: string; DESCRIPT?: string; @@ -84,8 +57,32 @@ export type ClasSchema = REFCLSNAME?: string; SHRM_ENABLED?: string; ABAP_LANGUAGE_VERSION?: string; - }; + }; + }; +} | { + abap: { + values: { + VSEOCLASS?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + STATE?: string; + CATEGORY?: string; + EXPOSURE?: string; + CLSFINAL?: string; + CLSABSTRCT?: string; + CLSCCINCL?: string; + FIXPT?: string; + UNICODE?: string; + WITH_UNIT_TESTS?: string; + DURATION?: string; + RISK?: string; + MSG_ID?: string; + REFCLSNAME?: string; + SHRM_ENABLED?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts index ccd3eb36..b9e048dd 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/devc.ts @@ -5,36 +5,33 @@ * Mode: Flattened */ -export type DevcSchema = - | { - abapGit: { +export type DevcSchema = { + abapGit: { abap: { - values: { - DEVC?: { - CTEXT: string; + values: { + DEVC?: { + CTEXT: string; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { DEVC?: { - CTEXT: string; + CTEXT: string; }; - }; - } - | { - abap: { + }; +} | { + abap: { values: { - DEVC?: { - CTEXT: string; - }; + DEVC?: { + CTEXT: string; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts index ecd26beb..96de4cbe 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/doma.ts @@ -5,77 +5,45 @@ * Mode: Flattened */ -export type DomaSchema = - | { - abapGit: { +export type DomaSchema = { + abapGit: { abap: { - values: { - DD01V?: { - DOMNAME: string; - DDLANGUAGE?: string; - DATATYPE?: string; - LENG?: string; - OUTPUTLEN?: string; - DECIMALS?: string; - LOWERCASE?: string; - SIGNFLAG?: string; - VALEXI?: string; - ENTITYTAB?: string; - CONVEXIT?: string; - DDTEXT?: string; - DOMMASTER?: string; - }; - DD07V_TAB?: { - DD07V?: { - DOMNAME?: string; - VALPOS?: string; - DDLANGUAGE?: string; - DOMVALUE_L?: string; - DOMVALUE_H?: string; - DDTEXT?: string; - }[]; + values: { + DD01V?: { + DOMNAME: string; + DDLANGUAGE?: string; + DATATYPE?: string; + LENG?: string; + OUTPUTLEN?: string; + DECIMALS?: string; + LOWERCASE?: string; + SIGNFLAG?: string; + VALEXI?: string; + ENTITYTAB?: string; + CONVEXIT?: string; + DDTEXT?: string; + DOMMASTER?: string; + }; + DD07V_TAB?: { + DD07V?: { + DOMNAME?: string; + VALPOS?: string; + DDLANGUAGE?: string; + DOMVALUE_L?: string; + DOMVALUE_H?: string; + DDTEXT?: string; + }[]; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { DD01V?: { - DOMNAME: string; - DDLANGUAGE?: string; - DATATYPE?: string; - LENG?: string; - OUTPUTLEN?: string; - DECIMALS?: string; - LOWERCASE?: string; - SIGNFLAG?: string; - VALEXI?: string; - ENTITYTAB?: string; - CONVEXIT?: string; - DDTEXT?: string; - DOMMASTER?: string; - }; - DD07V_TAB?: { - DD07V?: { - DOMNAME?: string; - VALPOS?: string; - DDLANGUAGE?: string; - DOMVALUE_L?: string; - DOMVALUE_H?: string; - DDTEXT?: string; - }[]; - }; - }; - } - | { - abap: { - values: { - DD01V?: { DOMNAME: string; DDLANGUAGE?: string; DATATYPE?: string; @@ -89,18 +57,47 @@ export type DomaSchema = CONVEXIT?: string; DDTEXT?: string; DOMMASTER?: string; - }; - DD07V_TAB?: { + }; + DD07V_TAB?: { DD07V?: { - DOMNAME?: string; - VALPOS?: string; - DDLANGUAGE?: string; - DOMVALUE_L?: string; - DOMVALUE_H?: string; - DDTEXT?: string; + DOMNAME?: string; + VALPOS?: string; + DDLANGUAGE?: string; + DOMVALUE_L?: string; + DOMVALUE_H?: string; + DDTEXT?: string; }[]; - }; + }; + }; +} | { + abap: { + values: { + DD01V?: { + DOMNAME: string; + DDLANGUAGE?: string; + DATATYPE?: string; + LENG?: string; + OUTPUTLEN?: string; + DECIMALS?: string; + LOWERCASE?: string; + SIGNFLAG?: string; + VALEXI?: string; + ENTITYTAB?: string; + CONVEXIT?: string; + DDTEXT?: string; + DOMMASTER?: string; + }; + DD07V_TAB?: { + DD07V?: { + DOMNAME?: string; + VALPOS?: string; + DDLANGUAGE?: string; + DOMVALUE_L?: string; + DOMVALUE_H?: string; + DDTEXT?: string; + }[]; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts index febce0dc..8e7e7365 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/dtel.ts @@ -5,71 +5,42 @@ * Mode: Flattened */ -export type DtelSchema = - | { - abapGit: { +export type DtelSchema = { + abapGit: { abap: { - values: { - DD04V?: { - ROLLNAME: string; - DDLANGUAGE?: string; - DOMNAME?: string; - HEADLEN?: string; - SCRLEN1?: string; - SCRLEN2?: string; - SCRLEN3?: string; - DDTEXT?: string; - REPTEXT?: string; - SCRTEXT_S?: string; - SCRTEXT_M?: string; - SCRTEXT_L?: string; - DTELMASTER?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; - OUTPUTLEN?: string; - REFKIND?: string; - REFTYPE?: string; - ABAP_LANGUAGE_VERSION?: string; + values: { + DD04V?: { + ROLLNAME: string; + DDLANGUAGE?: string; + DOMNAME?: string; + HEADLEN?: string; + SCRLEN1?: string; + SCRLEN2?: string; + SCRLEN3?: string; + DDTEXT?: string; + REPTEXT?: string; + SCRTEXT_S?: string; + SCRTEXT_M?: string; + SCRTEXT_L?: string; + DTELMASTER?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + OUTPUTLEN?: string; + REFKIND?: string; + REFTYPE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { DD04V?: { - ROLLNAME: string; - DDLANGUAGE?: string; - DOMNAME?: string; - HEADLEN?: string; - SCRLEN1?: string; - SCRLEN2?: string; - SCRLEN3?: string; - DDTEXT?: string; - REPTEXT?: string; - SCRTEXT_S?: string; - SCRTEXT_M?: string; - SCRTEXT_L?: string; - DTELMASTER?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; - OUTPUTLEN?: string; - REFKIND?: string; - REFTYPE?: string; - ABAP_LANGUAGE_VERSION?: string; - }; - }; - } - | { - abap: { - values: { - DD04V?: { ROLLNAME: string; DDLANGUAGE?: string; DOMNAME?: string; @@ -90,8 +61,34 @@ export type DtelSchema = REFKIND?: string; REFTYPE?: string; ABAP_LANGUAGE_VERSION?: string; - }; + }; + }; +} | { + abap: { + values: { + DD04V?: { + ROLLNAME: string; + DDLANGUAGE?: string; + DOMNAME?: string; + HEADLEN?: string; + SCRLEN1?: string; + SCRLEN2?: string; + SCRLEN3?: string; + DDTEXT?: string; + REPTEXT?: string; + SCRTEXT_S?: string; + SCRTEXT_M?: string; + SCRTEXT_L?: string; + DTELMASTER?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + OUTPUTLEN?: string; + REFKIND?: string; + REFTYPE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/fugr.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/fugr.ts index d93c9cea..7fd9cee3 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/fugr.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/fugr.ts @@ -6,44 +6,44 @@ */ export type FugrSchema = { - abapGit: { - abap: { - values: { - AREAT?: string; - INCLUDES?: { - SOBJ_NAME?: string[]; - }; - FUNCTIONS?: { - item?: { - FUNCNAME: string; - SHORT_TEXT?: string; - IMPORT?: { - RSIMP?: { - PARAMETER?: string; - TYP?: string; - DBFIELD?: string; - DEFAULT?: string; - OPTIONAL?: string; - REFERENCE?: string; - }[]; - }; - EXPORT?: { - RSEXP?: { - PARAMETER?: string; - TYP?: string; - DBFIELD?: string; - DEFAULT?: string; - OPTIONAL?: string; - REFERENCE?: string; - }[]; + abapGit: { + abap: { + values: { + AREAT?: string; + INCLUDES?: { + SOBJ_NAME?: string[]; + }; + FUNCTIONS?: { + item?: { + FUNCNAME: string; + SHORT_TEXT?: string; + IMPORT?: { + RSIMP?: { + PARAMETER?: string; + TYP?: string; + DBFIELD?: string; + DEFAULT?: string; + OPTIONAL?: string; + REFERENCE?: string; + }[]; + }; + EXPORT?: { + RSEXP?: { + PARAMETER?: string; + TYP?: string; + DBFIELD?: string; + DEFAULT?: string; + OPTIONAL?: string; + REFERENCE?: string; + }[]; + }; + }[]; + }; }; - }[]; + version?: string; }; - }; - version?: string; + version: string; + serializer: string; + serializer_version: string; }; - version: string; - serializer: string; - serializer_version: string; - }; }; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/index.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/index.ts index b4c005e6..411164c8 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/index.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/index.ts @@ -1,6 +1,6 @@ /** * Auto-generated types index - * + * * DO NOT EDIT - Generated by ts-xsd codegen */ diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts index e56597a8..c6fcdd6f 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/intf.ts @@ -5,45 +5,29 @@ * Mode: Flattened */ -export type IntfSchema = - | { - abapGit: { +export type IntfSchema = { + abapGit: { abap: { - values: { - VSEOINTERF?: { - CLSNAME: string; - LANGU?: string; - DESCRIPT?: string; - EXPOSURE?: string; - STATE?: string; - UNICODE?: string; - ABAP_LANGUAGE_VERSION?: string; + values: { + VSEOINTERF?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + EXPOSURE?: string; + STATE?: string; + UNICODE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { VSEOINTERF?: { - CLSNAME: string; - LANGU?: string; - DESCRIPT?: string; - EXPOSURE?: string; - STATE?: string; - UNICODE?: string; - ABAP_LANGUAGE_VERSION?: string; - }; - }; - } - | { - abap: { - values: { - VSEOINTERF?: { CLSNAME: string; LANGU?: string; DESCRIPT?: string; @@ -51,8 +35,21 @@ export type IntfSchema = STATE?: string; UNICODE?: string; ABAP_LANGUAGE_VERSION?: string; - }; + }; + }; +} | { + abap: { + values: { + VSEOINTERF?: { + CLSNAME: string; + LANGU?: string; + DESCRIPT?: string; + EXPOSURE?: string; + STATE?: string; + UNICODE?: string; + ABAP_LANGUAGE_VERSION?: string; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/prog.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/prog.ts index b3466adb..1f7e5f0b 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/prog.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/prog.ts @@ -6,32 +6,32 @@ */ export type ProgSchema = { - abapGit: { - abap: { - values: { - PROGDIR?: { - NAME: string; - STATE?: string; - SUBC?: string; - FIXPT?: string; - UNICODE?: string; - DTEFUNC?: string; - RLOAD?: string; - UCCHECK?: string; - ABAP_LANGUAGE_VERSION?: string; + abapGit: { + abap: { + values: { + PROGDIR?: { + NAME: string; + STATE?: string; + SUBC?: string; + FIXPT?: string; + UNICODE?: string; + DTEFUNC?: string; + RLOAD?: string; + UCCHECK?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + TPOOL?: { + item?: { + ID: string; + ENTRY?: string; + LENGTH?: string; + }[]; + }; + }; + version?: string; }; - TPOOL?: { - item?: { - ID: string; - ENTRY?: string; - LENGTH?: string; - }[]; - }; - }; - version?: string; + version: string; + serializer: string; + serializer_version: string; }; - version: string; - serializer: string; - serializer_version: string; - }; }; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts index 2d1ef8a8..17157b54 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/tabl.ts @@ -5,113 +5,63 @@ * Mode: Flattened */ -export type TablSchema = - | { - abapGit: { +export type TablSchema = { + abapGit: { abap: { - values: { - DD02V?: { - TABNAME: string; - DDLANGUAGE?: string; - TABCLASS?: string; - LANGDEP?: string; - CLIDEP?: string; - SQLTAB?: string; - DATCLASS?: string; - DDTEXT?: string; - MASTERLANG?: string; - BUFFERED?: string; - MATEFLAG?: string; - CONTFLAG?: string; - SHLPEXI?: string; - EXCLASS?: string; - AUTHCLASS?: string; + values: { + DD02V?: { + TABNAME: string; + DDLANGUAGE?: string; + TABCLASS?: string; + LANGDEP?: string; + CLIDEP?: string; + SQLTAB?: string; + DATCLASS?: string; + DDTEXT?: string; + MASTERLANG?: string; + BUFFERED?: string; + MATEFLAG?: string; + CONTFLAG?: string; + SHLPEXI?: string; + EXCLASS?: string; + AUTHCLASS?: string; + }; + DD03P_TABLE?: { + DD03P?: { + FIELDNAME?: string; + POSITION?: string; + KEYFLAG?: string; + ROLLNAME?: string; + ADMINFIELD?: string; + INTTYPE?: string; + INTLEN?: string; + REFTABLE?: string; + REFFIELD?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + NOTNULL?: string; + DOMNAME?: string; + PRECFIELD?: string; + MASK?: string; + DDTEXT?: string; + SHLPORIGIN?: string; + COMPTYPE?: string; + TABNAME?: string; + DDLANGUAGE?: string; + CONRFLAG?: string; + }[]; + }; }; - DD03P_TABLE?: { - DD03P?: { - TABNAME?: string; - FIELDNAME?: string; - DDLANGUAGE?: string; - POSITION?: string; - KEYFLAG?: string; - ROLLNAME?: string; - ADMINFIELD?: string; - INTTYPE?: string; - INTLEN?: string; - NOTNULL?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; - DOMNAME?: string; - SHLPORIGIN?: string; - MASK?: string; - COMPTYPE?: string; - REFTABLE?: string; - REFFIELD?: string; - CONRFLAG?: string; - PRECFIELD?: string; - DDTEXT?: string; - }[]; - }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { DD02V?: { - TABNAME: string; - DDLANGUAGE?: string; - TABCLASS?: string; - LANGDEP?: string; - CLIDEP?: string; - SQLTAB?: string; - DATCLASS?: string; - DDTEXT?: string; - MASTERLANG?: string; - BUFFERED?: string; - MATEFLAG?: string; - CONTFLAG?: string; - SHLPEXI?: string; - EXCLASS?: string; - AUTHCLASS?: string; - }; - DD03P_TABLE?: { - DD03P?: { - TABNAME?: string; - FIELDNAME?: string; - DDLANGUAGE?: string; - POSITION?: string; - KEYFLAG?: string; - ROLLNAME?: string; - ADMINFIELD?: string; - INTTYPE?: string; - INTLEN?: string; - NOTNULL?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; - DOMNAME?: string; - SHLPORIGIN?: string; - MASK?: string; - COMPTYPE?: string; - REFTABLE?: string; - REFFIELD?: string; - CONRFLAG?: string; - PRECFIELD?: string; - DDTEXT?: string; - }[]; - }; - }; - } - | { - abap: { - values: { - DD02V?: { TABNAME: string; DDLANGUAGE?: string; TABCLASS?: string; @@ -127,34 +77,81 @@ export type TablSchema = SHLPEXI?: string; EXCLASS?: string; AUTHCLASS?: string; - }; - DD03P_TABLE?: { + }; + DD03P_TABLE?: { DD03P?: { - TABNAME?: string; - FIELDNAME?: string; - DDLANGUAGE?: string; - POSITION?: string; - KEYFLAG?: string; - ROLLNAME?: string; - ADMINFIELD?: string; - INTTYPE?: string; - INTLEN?: string; - NOTNULL?: string; - DATATYPE?: string; - LENG?: string; - DECIMALS?: string; - DOMNAME?: string; - SHLPORIGIN?: string; - MASK?: string; - COMPTYPE?: string; - REFTABLE?: string; - REFFIELD?: string; - CONRFLAG?: string; - PRECFIELD?: string; - DDTEXT?: string; + FIELDNAME?: string; + POSITION?: string; + KEYFLAG?: string; + ROLLNAME?: string; + ADMINFIELD?: string; + INTTYPE?: string; + INTLEN?: string; + REFTABLE?: string; + REFFIELD?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + NOTNULL?: string; + DOMNAME?: string; + PRECFIELD?: string; + MASK?: string; + DDTEXT?: string; + SHLPORIGIN?: string; + COMPTYPE?: string; + TABNAME?: string; + DDLANGUAGE?: string; + CONRFLAG?: string; }[]; - }; + }; + }; +} | { + abap: { + values: { + DD02V?: { + TABNAME: string; + DDLANGUAGE?: string; + TABCLASS?: string; + LANGDEP?: string; + CLIDEP?: string; + SQLTAB?: string; + DATCLASS?: string; + DDTEXT?: string; + MASTERLANG?: string; + BUFFERED?: string; + MATEFLAG?: string; + CONTFLAG?: string; + SHLPEXI?: string; + EXCLASS?: string; + AUTHCLASS?: string; + }; + DD03P_TABLE?: { + DD03P?: { + FIELDNAME?: string; + POSITION?: string; + KEYFLAG?: string; + ROLLNAME?: string; + ADMINFIELD?: string; + INTTYPE?: string; + INTLEN?: string; + REFTABLE?: string; + REFFIELD?: string; + DATATYPE?: string; + LENG?: string; + DECIMALS?: string; + NOTNULL?: string; + DOMNAME?: string; + PRECFIELD?: string; + MASK?: string; + DDTEXT?: string; + SHLPORIGIN?: string; + COMPTYPE?: string; + TABNAME?: string; + DDLANGUAGE?: string; + CONRFLAG?: string; + }[]; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts b/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts index c3e142c7..bc4d826f 100644 --- a/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts +++ b/packages/adt-plugin-abapgit/src/schemas/generated/types/ttyp.ts @@ -5,59 +5,36 @@ * Mode: Flattened */ -export type TtypSchema = - | { - abapGit: { +export type TtypSchema = { + abapGit: { abap: { - values: { - DD40V?: { - TYPENAME: string; - DDLANGUAGE?: string; - ROWTYPE?: string; - ROWKIND?: string; - DATATYPE?: string; - ACCESSMODE?: string; - KEYDEF?: string; - KEYKIND?: string; - GENERIC?: string; - LENG?: string; - DECIMALS?: string; - DDTEXT?: string; - TYPELEN?: string; - DEFFDNAME?: string; + values: { + DD40V?: { + TYPENAME: string; + DDLANGUAGE?: string; + ROWTYPE?: string; + ROWKIND?: string; + DATATYPE?: string; + ACCESSMODE?: string; + KEYDEF?: string; + KEYKIND?: string; + GENERIC?: string; + LENG?: string; + DECIMALS?: string; + DDTEXT?: string; + TYPELEN?: string; + DEFFDNAME?: string; + }; }; - }; - version?: string; + version?: string; }; version: string; serializer: string; serializer_version: string; - }; - } - | { - values: { + }; +} | { + values: { DD40V?: { - TYPENAME: string; - DDLANGUAGE?: string; - ROWTYPE?: string; - ROWKIND?: string; - DATATYPE?: string; - ACCESSMODE?: string; - KEYDEF?: string; - KEYKIND?: string; - GENERIC?: string; - LENG?: string; - DECIMALS?: string; - DDTEXT?: string; - TYPELEN?: string; - DEFFDNAME?: string; - }; - }; - } - | { - abap: { - values: { - DD40V?: { TYPENAME: string; DDLANGUAGE?: string; ROWTYPE?: string; @@ -72,8 +49,28 @@ export type TtypSchema = DDTEXT?: string; TYPELEN?: string; DEFFDNAME?: string; - }; + }; + }; +} | { + abap: { + values: { + DD40V?: { + TYPENAME: string; + DDLANGUAGE?: string; + ROWTYPE?: string; + ROWKIND?: string; + DATATYPE?: string; + ACCESSMODE?: string; + KEYDEF?: string; + KEYKIND?: string; + GENERIC?: string; + LENG?: string; + DECIMALS?: string; + DDTEXT?: string; + TYPELEN?: string; + DEFFDNAME?: string; + }; }; version?: string; - }; }; +}; diff --git a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd index 613ad9ab..43cb3d5a 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd02v.xsd @@ -4,7 +4,7 @@ - + @@ -20,6 +20,6 @@ - + diff --git a/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd b/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd index bd746ce7..fdad046d 100644 --- a/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd +++ b/packages/adt-plugin-abapgit/xsd/types/dd03p.xsd @@ -2,32 +2,32 @@ - + - - + - - + + + - + + + - - + + - - - + From 50e120094377bef4ea9a760b2456d825c4513022 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 09:32:55 +0100 Subject: [PATCH 12/19] fix(cds-to-abapgit): detect CLIDEP for client-dependent tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tables with a key field of type CLNT (abap.clnt) or data element MANDT are client-dependent. Set CLIDEP=X in DD02V when detected. Adds 4 tests for CLIDEP detection covering: - key field with mandt data element - key field with abap.clnt builtin - structures (no key fields → no CLIDEP) - non-key mandt field (not client-dependent) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../src/lib/handlers/cds-to-abapgit.ts | 15 +++++++- .../tests/handlers/tabl.test.ts | 38 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index 5de33381..a43ca6ce 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -177,13 +177,26 @@ export function buildDD02V( return false; }); + // Detect client-dependent table: + // CLIDEP=X when any key field references data element MANDT or builtin type abap.clnt + const hasClientKeyField = def.members.some((m) => { + if ('kind' in m && m.kind === 'include') return false; + const f = m as FieldDefinition; + if (!f.isKey) return false; + if (f.type.kind === 'named') return f.type.name.toUpperCase() === 'MANDT'; + if (f.type.kind === 'builtin') + return (f.type as BuiltinTypeRef).name === 'clnt'; + return false; + }); + // Build result in standard abapGit DD02V field order: - // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS + // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS const result: DD02VData = {}; result.TABNAME = def.name.toUpperCase(); result.DDLANGUAGE = language; result.TABCLASS = tabclass; if (hasLanguageField) result.LANGDEP = 'X'; + if (hasClientKeyField) result.CLIDEP = 'X'; result.DDTEXT = description; result.MASTERLANG = language; if (deliveryClass) result.CONTFLAG = deliveryClass; diff --git a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts index b77e2de9..2643908b 100644 --- a/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts +++ b/packages/adt-plugin-abapgit/tests/handlers/tabl.test.ts @@ -267,7 +267,7 @@ describe('TABL handler', () => { assert.ok(xml.includes('E')); }); - it('does not emit CLIDEP (cannot determine reliably from CDS)', async () => { + it('emits CLIDEP=X when table has key field with abap.clnt type', async () => { const mock = createMockTable({ name: 'ZTABLE_DTEL', description: 'Table with data elements', @@ -277,9 +277,8 @@ describe('TABL handler', () => { const files = await handler!.serialize(mock as any); const xml = files[0].content; - // CLIDEP is a DD02V database value — abapGit reads it from SAP. - // We cannot reliably determine it from CDS source alone. - assert.ok(!xml.includes('')); + // CLIDEP=X when any key field uses abap.clnt or data element MANDT + assert.ok(xml.includes('X')); }); }); @@ -641,6 +640,37 @@ describe('CDS-to-abapGit DD02V LANGDEP', () => { }); }); +describe('CDS-to-abapGit DD02V CLIDEP', () => { + it('buildDD02V sets CLIDEP=X when table has key field with mandt data element', () => { + const { ast } = parse(CDS_TRANSPARENT_TABLE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'AGE Test Transparent Table'); + assert.strictEqual(dd02v.CLIDEP, 'X'); + }); + + it('buildDD02V sets CLIDEP=X when table has key field with abap.clnt builtin', () => { + const { ast } = parse(CDS_TABLE_DATA_ELEMENTS); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'Table with data elements'); + assert.strictEqual(dd02v.CLIDEP, 'X'); + }); + + it('buildDD02V omits CLIDEP for structures (no key fields)', () => { + const { ast } = parse(CDS_STRUCTURE); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'AGE Test Structure'); + assert.strictEqual(dd02v.CLIDEP, undefined); + }); + + it('buildDD02V omits CLIDEP when structure has non-key mandt field', () => { + // CDS_ALL_TYPES has `client : mandt` but it's NOT a key field + const { ast } = parse(CDS_ALL_TYPES); + const def = ast.definitions[0] as any; + const dd02v = buildDD02V(def, 'E', 'Simple structure'); + assert.strictEqual(dd02v.CLIDEP, undefined); + }); +}); + describe('CDS-to-abapGit include directives', () => { it('generates .INCLUDE entry for plain include', async () => { const { ast } = parse(CDS_WITH_INCLUDES); From c0c312819c9e114952addd41b9807c6a7dd70ae8 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 09:33:05 +0100 Subject: [PATCH 13/19] feat(adt-diff): support multi-file and glob patterns in diff command Change diff argument from single required to variadic [files...] with glob expansion. Enables usage like: adt diff *.tabl.xml adt diff zage_tabl.tabl.xml zage_value_table.tabl.xml Multi-file mode shows per-object headers and aggregate summary. Single-file mode preserves the original detailed output. Exit code 1 if any differences found. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- packages/adt-diff/src/commands/diff.ts | 545 ++++++++++++++++--------- 1 file changed, 344 insertions(+), 201 deletions(-) diff --git a/packages/adt-diff/src/commands/diff.ts b/packages/adt-diff/src/commands/diff.ts index ee4a789a..a74b4899 100644 --- a/packages/adt-diff/src/commands/diff.ts +++ b/packages/adt-diff/src/commands/diff.ts @@ -17,13 +17,14 @@ * * Usage: * adt diff zage_structure.tabl.xml - * adt diff zcl_myclass.clas.xml - * adt diff zif_myintf.intf.xml --no-color + * adt diff *.tabl.xml + * adt diff zcl_myclass.clas.xml --no-color */ import type { CliCommandPlugin, CliContext } from '@abapify/adt-plugin'; -import { createAdk, type AdtClient } from '@abapify/adk'; +import { createAdk, type AdtClient, type AdkFactory } from '@abapify/adk'; import { readFileSync, existsSync, readdirSync } from 'node:fs'; +import { glob as nativeGlob } from 'node:fs/promises'; import { resolve, basename, dirname, join } from 'node:path'; import { createTwoFilesPatch } from 'diff'; import chalk from 'chalk'; @@ -35,6 +36,24 @@ import { } from '@abapify/adt-plugin-abapgit'; import { tablXmlToCdsDdl } from '../lib/abapgit-to-cds'; +/** + * Expand glob patterns to matching file paths. + * Passes through literal filenames unchanged. + */ +async function expandGlobs(patterns: string[], cwd: string): Promise { + const results: string[] = []; + for (const pattern of patterns) { + if (/[*?[\]{}]/.test(pattern)) { + for await (const match of nativeGlob(pattern, { cwd })) { + results.push(match); + } + } else { + results.push(pattern); + } + } + return results; +} + /** * Collect all local files belonging to one abapGit object. * @@ -222,6 +241,269 @@ function printDiff( return true; } +/** Result of diffing a single file */ +interface DiffResult { + objectName: string; + objectType: string; + hasDifferences: boolean; + fileCount: number; + identicalCount: number; + error?: string; +} + +/** + * Diff a single abapGit XML file against SAP remote. + * Returns a result object indicating whether differences were found. + */ +async function diffSingleFile( + filePath: string, + ctx: CliContext, + adk: AdkFactory, + options: { + contextLines: number; + useColor: boolean; + format: string; + }, +): Promise { + const { contextLines, useColor, format } = options; + const fullPath = resolve(ctx.cwd, filePath); + + if (!existsSync(fullPath)) { + return { + objectName: filePath, + objectType: '?', + hasDifferences: false, + fileCount: 0, + identicalCount: 0, + error: `File not found: ${fullPath}`, + }; + } + + // Parse filename to detect type + const filename = basename(fullPath); + const parsed = parseAbapGitFilename(filename); + if (!parsed) { + return { + objectName: filename, + objectType: '?', + hasDifferences: false, + fileCount: 0, + identicalCount: 0, + error: `Cannot parse filename: ${filename}. Expected abapGit format: name.type.xml`, + }; + } + + if (parsed.extension !== 'xml') { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: 0, + identicalCount: 0, + error: `Expected .xml metadata file, got .${parsed.extension}. Pass the .xml file, not .abap.`, + }; + } + + // Validate --format option + if (format === 'ddl' && parsed.type !== 'TABL') { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: 0, + identicalCount: 0, + error: `DDL format is only supported for TABL objects. Got: ${parsed.type}`, + }; + } + + // Look up handler from abapGit registry + const handler = getHandler(parsed.type); + if (!handler) { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: 0, + identicalCount: 0, + error: `Unsupported object type: ${parsed.type}. Supported: ${getSupportedTypes().join(', ')}`, + }; + } + + // Collect local files for this object + const objectName = parsed.name.toLowerCase(); + const localFiles = collectLocalFiles( + fullPath, + objectName, + handler.fileExtension, + ); + + console.log( + `\n${useColor ? chalk.bold('Diff:') : 'Diff:'} ${parsed.name} (${parsed.type}) — ${localFiles.size} file(s)`, + ); + + // Parse local XML to extract ADK type info via fromAbapGit + const localXml = readFileSync(fullPath, 'utf-8'); + let adkType = parsed.type; + + if (handler.fromAbapGit) { + try { + const parsedXml = handler.schema.parse(localXml); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const values = (parsedXml as any)?.abapGit?.abap?.values ?? {}; + const payload = handler.fromAbapGit(values); + if (typeof payload.type === 'string') { + adkType = payload.type; + } + } catch { + // Fall through — use filename-derived type + } + } + + // Fetch remote ADK object + console.log( + `${useColor ? chalk.dim('Fetching') : 'Fetching'} ${parsed.name} (${adkType}) from SAP...`, + ); + + const remoteObj = adk.get(parsed.name, adkType); + + try { + await remoteObj.load(); + } catch (error) { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: localFiles.size, + identicalCount: 0, + error: `Failed to load remote object: ${error instanceof Error ? error.message : String(error)}`, + }; + } + + // ======================================== + // DDL format: compare CDS DDL source + // ======================================== + if (format === 'ddl') { + const localDdl = tablXmlToCdsDdl(localXml); + + let remoteDdl: string; + try { + remoteDdl = await remoteObj.getSource(); + } catch (error) { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: 1, + identicalCount: 0, + error: `Failed to fetch remote DDL source: ${error instanceof Error ? error.message : String(error)}`, + }; + } + + const ddlFile = `${objectName}.tabl.acds`; + const diffFound = printDiff( + ddlFile, + ddlFile, + localDdl, + remoteDdl, + contextLines, + useColor, + ); + + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: diffFound, + fileCount: 1, + identicalCount: diffFound ? 0 : 1, + }; + } + + // Serialize remote using the same handler → produces SerializedFile[] + let remoteFiles; + try { + remoteFiles = await handler.serialize(remoteObj); + } catch (error) { + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences: false, + fileCount: localFiles.size, + identicalCount: 0, + error: `Failed to serialize remote object: ${error instanceof Error ? error.message : String(error)}`, + }; + } + + // Build remote file map (lowercase path → content) + const remoteMap = new Map(); + for (const f of remoteFiles) { + remoteMap.set(f.path.toLowerCase(), f.content); + } + + // Diff each file + let hasDifferences = false; + let identicalCount = 0; + + for (const [localPath, localContent] of localFiles) { + const remoteContent = remoteMap.get(localPath); + if (remoteContent === undefined) { + console.log( + useColor + ? chalk.yellow(`\n + Only in local: ${localPath}`) + : `\n + Only in local: ${localPath}`, + ); + hasDifferences = true; + continue; + } + + // For XML files: normalize both through schema, projecting remote + // onto local's field set to strip serializer-added extras + const isXml = localPath.endsWith('.xml'); + let diffLocal = localContent; + let diffRemote = remoteContent; + if (isXml) { + [diffLocal, diffRemote] = normalizeXmlPair( + localContent, + remoteContent, + handler, + ); + } + + const diffFound = printDiff( + localPath, + localPath, + diffLocal, + diffRemote, + contextLines, + useColor, + ); + if (diffFound) { + hasDifferences = true; + } else { + identicalCount++; + } + } + + // Files present in remote but not locally + for (const [remotePath] of remoteMap) { + if (!localFiles.has(remotePath)) { + console.log( + useColor + ? chalk.yellow(`\n - Only in remote: ${remotePath}`) + : `\n - Only in remote: ${remotePath}`, + ); + hasDifferences = true; + } + } + + return { + objectName: parsed.name, + objectType: parsed.type, + hasDifferences, + fileCount: localFiles.size, + identicalCount, + }; +} + export const diffCommand: CliCommandPlugin = { name: 'diff', description: @@ -229,8 +511,8 @@ export const diffCommand: CliCommandPlugin = { arguments: [ { - name: '', - description: `Local .xml file to compare (e.g., zcl_myclass.clas.xml). Supported types: ${getSupportedTypes().join(', ')}`, + name: '[files...]', + description: `Local .xml files or glob patterns to compare (e.g., zcl_myclass.clas.xml, *.tabl.xml). Supported types: ${getSupportedTypes().join(', ')}`, }, ], @@ -253,31 +535,14 @@ export const diffCommand: CliCommandPlugin = { ], async execute(args: Record, ctx: CliContext) { - const filePath = args.file as string; + const filePatterns = (args.files as string[]) ?? []; const contextLines = parseInt(String(args.context ?? '3'), 10); const useColor = args.color !== false; const format = String(args.format ?? 'xml').toLowerCase(); - // Resolve file path - const fullPath = resolve(ctx.cwd, filePath); - if (!existsSync(fullPath)) { - ctx.logger.error(`File not found: ${fullPath}`); - process.exit(1); - } - - // Parse filename to detect type - const filename = basename(fullPath); - const parsed = parseAbapGitFilename(filename); - if (!parsed) { + if (filePatterns.length === 0) { ctx.logger.error( - `Cannot parse filename: ${filename}. Expected abapGit format: name.type.xml`, - ); - process.exit(1); - } - - if (parsed.extension !== 'xml') { - ctx.logger.error( - `Expected .xml metadata file, got .${parsed.extension}. Pass the .xml file, not .abap.`, + 'No files specified. Usage: adt diff or adt diff *.tabl.xml', ); process.exit(1); } @@ -288,218 +553,96 @@ export const diffCommand: CliCommandPlugin = { process.exit(1); } - if (format === 'ddl' && parsed.type !== 'TABL') { - ctx.logger.error( - `DDL format is only supported for TABL objects. Got: ${parsed.type}`, - ); - process.exit(1); - } - - // Look up handler from abapGit registry - const handler = getHandler(parsed.type); - if (!handler) { - ctx.logger.error( - `Unsupported object type: ${parsed.type}. Supported: ${getSupportedTypes().join(', ')}`, - ); + // Expand glob patterns + const files = await expandGlobs(filePatterns, ctx.cwd); + if (files.length === 0) { + ctx.logger.error('No files matched the given pattern(s).'); process.exit(1); } - // Collect local files for this object - const objectName = parsed.name.toLowerCase(); - const localFiles = collectLocalFiles( - fullPath, - objectName, - handler.fileExtension, - ); - - console.log( - `\n${useColor ? chalk.bold('Diff:') : 'Diff:'} ${parsed.name} (${parsed.type}) — ${localFiles.size} file(s)`, - ); - // Need ADT client for remote comparison if (!ctx.getAdtClient) { ctx.logger.error('ADT client not available. Run: adt auth login'); process.exit(1); } - // Parse local XML to extract ADK type info via fromAbapGit - const localXml = readFileSync(fullPath, 'utf-8'); - let adkType = parsed.type; - - if (handler.fromAbapGit) { - try { - const parsedXml = handler.schema.parse(localXml); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const values = (parsedXml as any)?.abapGit?.abap?.values ?? {}; - const payload = handler.fromAbapGit(values); - if (typeof payload.type === 'string') { - adkType = payload.type; - } - } catch { - // Fall through — use filename-derived type - } - } - - // Fetch remote ADK object - console.log( - `${useColor ? chalk.dim('Fetching') : 'Fetching'} ${parsed.name} (${adkType}) from SAP...`, - ); - + // Create ADT client and ADK once, shared across all files const client = await ctx.getAdtClient!(); const adk = createAdk(client as AdtClient); - const remoteObj = adk.get(parsed.name, adkType); - - try { - await remoteObj.load(); - } catch (error) { - ctx.logger.error( - `Failed to load remote object: ${error instanceof Error ? error.message : String(error)}`, - ); - process.exit(1); - } - - // ======================================== - // DDL format: compare CDS DDL source - // ======================================== - if (format === 'ddl') { - // Local: convert abapGit XML → CDS DDL - const localXml = readFileSync(fullPath, 'utf-8'); - const localDdl = tablXmlToCdsDdl(localXml); - - // Remote: fetch CDS source directly from SAP - let remoteDdl: string; - try { - remoteDdl = await remoteObj.getSource(); - } catch (error) { - ctx.logger.error( - `Failed to fetch remote DDL source: ${error instanceof Error ? error.message : String(error)}`, - ); - process.exit(1); - } - const ddlFile = `${objectName}.tabl.acds`; - const diffFound = printDiff( - ddlFile, - ddlFile, - localDdl, - remoteDdl, + // Diff each file + const results: DiffResult[] = []; + for (const file of files) { + const result = await diffSingleFile(file, ctx, adk, { contextLines, useColor, - ); + format, + }); - console.log(''); - if (diffFound) { - console.log( - useColor ? chalk.red('Differences found.') : 'Differences found.', - ); - process.exit(1); - } else { - console.log( - useColor - ? chalk.green('No differences found. (DDL source identical)') - : 'No differences found. (DDL source identical)', - ); + if (result.error) { + ctx.logger.error(result.error); } - return; - } - - // Serialize remote using the same handler → produces SerializedFile[] - let remoteFiles; - try { - remoteFiles = await handler.serialize(remoteObj); - } catch (error) { - ctx.logger.error( - `Failed to serialize remote object: ${error instanceof Error ? error.message : String(error)}`, - ); - process.exit(1); - } - // Build remote file map (lowercase path → content) - const remoteMap = new Map(); - for (const f of remoteFiles) { - remoteMap.set(f.path.toLowerCase(), f.content); + results.push(result); } - // Diff each file - let hasDifferences = false; - let identicalCount = 0; - - for (const [localPath, localContent] of localFiles) { - const remoteContent = remoteMap.get(localPath); - if (remoteContent === undefined) { - console.log( - useColor - ? chalk.yellow(`\n + Only in local: ${localPath}`) - : `\n + Only in local: ${localPath}`, - ); - hasDifferences = true; - continue; - } - - // For XML files: normalize both through schema, projecting remote - // onto local's field set to strip serializer-added extras - const isXml = localPath.endsWith('.xml'); - let diffLocal = localContent; - let diffRemote = remoteContent; - if (isXml) { - [diffLocal, diffRemote] = normalizeXmlPair( - localContent, - remoteContent, - handler, - ); - } + // Summary + const totalFiles = results.length; + const withDiffs = results.filter((r) => r.hasDifferences).length; + const withErrors = results.filter((r) => r.error).length; + const identical = totalFiles - withDiffs - withErrors; - const diffFound = printDiff( - localPath, - localPath, - diffLocal, - diffRemote, - contextLines, - useColor, - ); - if (diffFound) { - hasDifferences = true; - } else { - identicalCount++; + console.log(''); + if (totalFiles === 1) { + // Single-file mode: keep original concise output + const r = results[0]; + if (r.error) { + // Error already printed above + process.exit(1); } - } - - // Files present in remote but not locally - for (const [remotePath] of remoteMap) { - if (!localFiles.has(remotePath)) { + if (r.hasDifferences) { console.log( useColor - ? chalk.yellow(`\n - Only in remote: ${remotePath}`) - : `\n - Only in remote: ${remotePath}`, + ? chalk.red('Differences found.') + + (r.identicalCount > 0 + ? chalk.dim(` (${r.identicalCount} file(s) identical)`) + : '') + : `Differences found.${r.identicalCount > 0 ? ` (${r.identicalCount} file(s) identical)` : ''}`, ); - hasDifferences = true; + process.exit(1); } - } - - // Summary - console.log(''); - if (!hasDifferences) { console.log( useColor ? chalk.green( - `No differences found. (${identicalCount} file(s) identical)`, + `No differences found. (${r.identicalCount} file(s) identical)`, ) - : `No differences found. (${identicalCount} file(s) identical)`, + : `No differences found. (${r.identicalCount} file(s) identical)`, ); return; } + // Multi-file summary + const parts: string[] = [ + `${totalFiles} object(s) checked`, + `${identical} identical`, + ]; + if (withDiffs > 0) parts.push(`${withDiffs} with differences`); + if (withErrors > 0) parts.push(`${withErrors} with errors`); + const summary = parts.join(', '); + + if (withDiffs > 0 || withErrors > 0) { + console.log( + useColor + ? chalk.red(`Diff summary: ${summary}`) + : `Diff summary: ${summary}`, + ); + process.exit(1); + } console.log( useColor - ? chalk.red('Differences found.') + - (identicalCount > 0 - ? chalk.dim(` (${identicalCount} file(s) identical)`) - : '') - : `Differences found.${identicalCount > 0 ? ` (${identicalCount} file(s) identical)` : ''}`, + ? chalk.green(`Diff summary: ${summary}`) + : `Diff summary: ${summary}`, ); - - // Exit with non-zero if differences exist (standard diff convention) - process.exit(1); }, }; From 591ea22468776daccd0e9bc1c62a95523869a0e1 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 09:43:14 +0100 Subject: [PATCH 14/19] fix(adt-diff): align zage_tabl test expectations with fixture Fixture only has 1 key field (CLIENT), tests expected 2 key fields and a 'key_field' that doesn't exist in the current fixture. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- packages/adt-diff/tests/abapgit-to-cds.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/adt-diff/tests/abapgit-to-cds.test.ts b/packages/adt-diff/tests/abapgit-to-cds.test.ts index bbca8dde..57aece1b 100644 --- a/packages/adt-diff/tests/abapgit-to-cds.test.ts +++ b/packages/adt-diff/tests/abapgit-to-cds.test.ts @@ -527,9 +527,10 @@ describe('parseTablXml', () => { expect(dd02v.TABCLASS).toBe('TRANSP'); expect(dd02v.CONTFLAG).toBe('A'); - // Has key fields + // Has key fields (CLIENT is the only field) const keyFields = dd03p.filter((f) => f.KEYFLAG === 'X'); - expect(keyFields.length).toBe(2); + expect(keyFields.length).toBe(1); + expect(keyFields[0].FIELDNAME).toBe('CLIENT'); }); }); @@ -632,10 +633,10 @@ describe('tablXmlToCdsDdl', () => { '@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE', ); - // Key fields - expect(ddl).toContain('key mandt'); + // Key fields (CLIENT is the only field, builtin CLNT type) + expect(ddl).toContain('key client'); + expect(ddl).toContain('abap.clnt'); expect(ddl).toContain('not null'); - expect(ddl).toContain('key key_field'); }); it('should convert zage_structure1.tabl.xml to CDS DDL', () => { From 83e86849aecf0d461cba0f08a3beb4cd563c674f Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 09:43:29 +0100 Subject: [PATCH 15/19] chore: unify AI agent rules under .agents/rules/ Move 5 rules from .windsurf/rules/ into .agents/rules/ organized by domain (development/, planning/, git/). Both .windsurf/rules/ and .cognition/rules/ are now symlinks to .agents/rules/, keeping all AI tools (Windsurf, Devin, Claude) in sync from a single source. Added git/no-auto-commit.md: require explicit user approval before any git commit or push. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../rules/development}/bundler-imports.md | 0 .../rules/development}/tmp-folder-testing.md | 0 .../development/tooling}/nx-monorepo-setup.md | 0 .agents/rules/git/no-auto-commit.md | 30 +++++++++++++++++++ .../planning}/project-planning-memory.md | 0 .../rules/planning}/spec-first-then-code.md | 0 .cognition/rules | 1 + .windsurf/rules | 1 + 8 files changed, 32 insertions(+) rename {.windsurf/rules => .agents/rules/development}/bundler-imports.md (100%) rename {.windsurf/rules => .agents/rules/development}/tmp-folder-testing.md (100%) rename {.windsurf/rules => .agents/rules/development/tooling}/nx-monorepo-setup.md (100%) create mode 100644 .agents/rules/git/no-auto-commit.md rename {.windsurf/rules => .agents/rules/planning}/project-planning-memory.md (100%) rename {.windsurf/rules => .agents/rules/planning}/spec-first-then-code.md (100%) create mode 120000 .cognition/rules create mode 120000 .windsurf/rules diff --git a/.windsurf/rules/bundler-imports.md b/.agents/rules/development/bundler-imports.md similarity index 100% rename from .windsurf/rules/bundler-imports.md rename to .agents/rules/development/bundler-imports.md diff --git a/.windsurf/rules/tmp-folder-testing.md b/.agents/rules/development/tmp-folder-testing.md similarity index 100% rename from .windsurf/rules/tmp-folder-testing.md rename to .agents/rules/development/tmp-folder-testing.md diff --git a/.windsurf/rules/nx-monorepo-setup.md b/.agents/rules/development/tooling/nx-monorepo-setup.md similarity index 100% rename from .windsurf/rules/nx-monorepo-setup.md rename to .agents/rules/development/tooling/nx-monorepo-setup.md diff --git a/.agents/rules/git/no-auto-commit.md b/.agents/rules/git/no-auto-commit.md new file mode 100644 index 00000000..24e6e78c --- /dev/null +++ b/.agents/rules/git/no-auto-commit.md @@ -0,0 +1,30 @@ +--- +trigger: always_on +description: Never commit or push without explicit user approval. +--- + +# No Auto-Commit Rule + +## Rule + +**NEVER run `git commit` or `git push` without explicit user approval.** + +Before committing: + +1. Show the planned commit(s): message, staged files, diff summary +2. Wait for the user to confirm +3. Only then execute `git commit` + +## Applies To + +- All AI assistants (Devin, Windsurf, Claude, etc.) +- Both interactive and background/subagent sessions + +## Why + +Commits are permanent project history. The user must review and approve: + +- What files are staged +- The commit message +- Whether changes should be split into multiple commits +- Whether to push after committing diff --git a/.windsurf/rules/project-planning-memory.md b/.agents/rules/planning/project-planning-memory.md similarity index 100% rename from .windsurf/rules/project-planning-memory.md rename to .agents/rules/planning/project-planning-memory.md diff --git a/.windsurf/rules/spec-first-then-code.md b/.agents/rules/planning/spec-first-then-code.md similarity index 100% rename from .windsurf/rules/spec-first-then-code.md rename to .agents/rules/planning/spec-first-then-code.md diff --git a/.cognition/rules b/.cognition/rules new file mode 120000 index 00000000..2d5c9a97 --- /dev/null +++ b/.cognition/rules @@ -0,0 +1 @@ +../.agents/rules \ No newline at end of file diff --git a/.windsurf/rules b/.windsurf/rules new file mode 120000 index 00000000..2d5c9a97 --- /dev/null +++ b/.windsurf/rules @@ -0,0 +1 @@ +../.agents/rules \ No newline at end of file From d7ba7b90a1b1ced8af6205dc91956c50e5f5dd7a Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 09:48:24 +0100 Subject: [PATCH 16/19] chore: add Windsurf frontmatter triggers to all agent rules - bundler-imports: fix glob *.ts -> **/*.ts (recursive) - package-versions: add trigger: model_decision - nx-circular-dependencies: add trigger: model_decision - nx-monorepo-setup: add trigger: model_decision - project-planning-memory: add description to frontmatter Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .agents/rules/development/bundler-imports.md | 2 +- .agents/rules/development/package-versions.md | 5 +++++ .../rules/development/tooling/nx-circular-dependencies.md | 5 +++++ .agents/rules/development/tooling/nx-monorepo-setup.md | 5 +++++ .agents/rules/planning/project-planning-memory.md | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.agents/rules/development/bundler-imports.md b/.agents/rules/development/bundler-imports.md index 06105677..73480e2c 100644 --- a/.agents/rules/development/bundler-imports.md +++ b/.agents/rules/development/bundler-imports.md @@ -1,7 +1,7 @@ --- trigger: glob description: Enforce extensionless imports for internal files in bundled TypeScript packages. -globs: *.ts +globs: '**/*.ts' --- ## Rule: Use Extensionless Imports for Internal Files diff --git a/.agents/rules/development/package-versions.md b/.agents/rules/development/package-versions.md index 3ab61791..ad6b3def 100644 --- a/.agents/rules/development/package-versions.md +++ b/.agents/rules/development/package-versions.md @@ -1,3 +1,8 @@ +--- +trigger: model_decision +description: Always install latest package versions when adding dependencies. Never hardcode specific versions. +--- + # Package Versions: Always Install Latest ## Rule diff --git a/.agents/rules/development/tooling/nx-circular-dependencies.md b/.agents/rules/development/tooling/nx-circular-dependencies.md index 8c6a2baf..099f6e85 100644 --- a/.agents/rules/development/tooling/nx-circular-dependencies.md +++ b/.agents/rules/development/tooling/nx-circular-dependencies.md @@ -1,3 +1,8 @@ +--- +trigger: model_decision +description: Fix false circular dependencies in Nx caused by config files. Use .nxignore and ESLint ignores. +--- + # Nx Circular Dependencies: Config File Exclusions ## Problem diff --git a/.agents/rules/development/tooling/nx-monorepo-setup.md b/.agents/rules/development/tooling/nx-monorepo-setup.md index f4dfd62f..b5e4d11a 100644 --- a/.agents/rules/development/tooling/nx-monorepo-setup.md +++ b/.agents/rules/development/tooling/nx-monorepo-setup.md @@ -1,3 +1,8 @@ +--- +trigger: model_decision +description: Nx monorepo setup rules - package creation workflow, config templates, plugin inference, import conventions. +--- + # Nx Monorepo Setup Rules > **See**: [`.agents/rules/development/tooling/nx-monorepo.md`](../../../.agents/rules/development/tooling/nx-monorepo.md) for complete Nx plugin inference system documentation. diff --git a/.agents/rules/planning/project-planning-memory.md b/.agents/rules/planning/project-planning-memory.md index 2603841f..99dc580c 100644 --- a/.agents/rules/planning/project-planning-memory.md +++ b/.agents/rules/planning/project-planning-memory.md @@ -1,5 +1,6 @@ --- trigger: always_on +description: OpenSpec-based project planning. Check openspec/specs/ and openspec/changes/ before making changes. --- # Project Planning and Memory Persistence Rule From 1bf97517b2ebcac353c538b30023da02c120c33f Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 10:05:37 +0100 Subject: [PATCH 17/19] chore: remove redundant agent rules, consolidate into AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete 4 agent rule files (nx-circular-dependencies, nx-monorepo-setup, project-planning-memory, spec-first-then-code) and merge their content into AGENTS.md as a single source of truth. AGENTS.md now covers: - Nx monorepo workflow (build/test/lint commands) - Package layout and dependency graph - Type flow architecture (XSD → schemas → contracts → client) - Plugin system and ADK object handlers - Save lifecycle and source comparison --- .agents/rules/adt/adk-save-logic.md | 24 ++ .agents/rules/adt/adt-ddic-mapping.md | 39 +++ .agents/rules/adt/xsd-best-practices.md | 30 +++ .../rules/development/coding-conventions.md | 16 ++ .agents/rules/development/file-lifecycle.md | 21 ++ .../nx-circular-dependencies.md | 0 .../tooling => nx}/nx-monorepo-setup.md | 0 .../project-planning-memory.md | 0 .../spec-first-then-code.md | 0 .agents/rules/verification/after-changes.md | 14 + AGENTS.md | 252 ++++-------------- git_modules/abapgit-examples | 2 +- 12 files changed, 203 insertions(+), 195 deletions(-) create mode 100644 .agents/rules/adt/adk-save-logic.md create mode 100644 .agents/rules/adt/adt-ddic-mapping.md create mode 100644 .agents/rules/adt/xsd-best-practices.md create mode 100644 .agents/rules/development/coding-conventions.md create mode 100644 .agents/rules/development/file-lifecycle.md rename .agents/rules/{development/tooling => nx}/nx-circular-dependencies.md (100%) rename .agents/rules/{development/tooling => nx}/nx-monorepo-setup.md (100%) rename .agents/rules/{planning => openspec}/project-planning-memory.md (100%) rename .agents/rules/{planning => openspec}/spec-first-then-code.md (100%) create mode 100644 .agents/rules/verification/after-changes.md diff --git a/.agents/rules/adt/adk-save-logic.md b/.agents/rules/adt/adk-save-logic.md new file mode 100644 index 00000000..7cb132f2 --- /dev/null +++ b/.agents/rules/adt/adk-save-logic.md @@ -0,0 +1,24 @@ +--- +trigger: model_decision +description: Complex logic handling for ADK object save, upsert, and locking. +--- + +# ADK Save and Upsert Logic + +## Upsert Fallback Handling +`AdkObject.save()` upsert fallback has **TWO** catch paths that both need `422 "already exists"` handling to correctly mark objects as unchanged instead of failing. + +1. **Lock Catch:** + - If Lock fails -> `shouldFallbackToCreate(e)` -> try create. + - Catch `422 "already exists"` -> mark unchanged. + +2. **Outer saveViaContract Catch:** + - If Lock succeeds but PUT fails (e.g. `405 Method Not Allowed`) -> `shouldFallbackToCreate(e)` -> try create. + - Catch `422 "already exists"` -> mark unchanged. + +**Note:** Both paths must wrap their `save({ mode: 'create' })` calls in try/catch blocks that check `isAlreadyExistsError(e)`. + +## Endpoint Behaviors +- **TABL Lock:** Can succeed for existing objects, but subsequent metadata PUT returns `405 Method Not Allowed`. +- **TTYP Lock:** Returns `405` when object doesn't exist (instead of `404`). +- **Create (POST):** Returns `422` (or "Unprocessable") with "already exists" message if object exists. diff --git a/.agents/rules/adt/adt-ddic-mapping.md b/.agents/rules/adt/adt-ddic-mapping.md new file mode 100644 index 00000000..974b188b --- /dev/null +++ b/.agents/rules/adt/adt-ddic-mapping.md @@ -0,0 +1,39 @@ +--- +trigger: model_decision +description: Mapping of SAP ADT DDIC objects to their root elements and schemas. +--- + +# SAP ADT DDIC Object Mapping + +SAP ADT wraps DDIC object responses in different root elements depending on the object type. + +## Mappings + +### DOMA (Domain) +- **Root Element:** `domain` +- **Namespace:** `http://www.sap.com/adt/dictionary/domains` +- **Schema:** `sap/domain.xsd` +- **Notes:** Direct extension of `adtcore:AdtMainObject`. Works out of the box. + +### DTEL (Data Element) +- **Root Element:** `blue:wbobj` +- **Namespace:** `http://www.sap.com/wbobj/dictionary/dtel` +- **Inner Content:** Wraps `dtel:dataElement`. +- **Schema:** `.xsd/custom/dataelementWrapper.xsd` +- **ADK wrapperKey:** `'wbobj'` + +### TABL (Table) / Structure +- **Root Element:** `blue:blueSource` +- **Namespace:** `http://www.sap.com/wbobj/blue` +- **Schema:** `.xsd/custom/blueSource.xsd` +- **ADK wrapperKey:** `'blueSource'` +- **Notes:** Extends `abapsource:AbapSourceMainObject`. Contracts use `crud()` helper. + +### TTYP (Table Type) +- **Root Element:** `tableType` +- **Namespace:** `http://www.sap.com/dictionary/tabletype` +- **Schema:** `sap/tabletype.xsd` +- **Notes:** Direct extension of `adtcore:AdtMainObject`. Works out of the box. + +## Locking +- **Header:** Lock operations require `Accept: application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result`. diff --git a/.agents/rules/adt/xsd-best-practices.md b/.agents/rules/adt/xsd-best-practices.md new file mode 100644 index 00000000..7819620b --- /dev/null +++ b/.agents/rules/adt/xsd-best-practices.md @@ -0,0 +1,30 @@ +--- +trigger: model_decision +description: Best practices for working with XSD files and XML parsing in this project. +--- + +# XSD and XML Best Practices + +## XSD Validity +**CRITICAL:** Never create broken/invalid XSD files and then modify ts-xsd to handle them. XSDs must be valid W3C XML Schema documents FIRST. +- `xs:import` = cross-namespace. Only ONE `xs:import` per namespace per schema. +- `xs:include` = same-namespace composition/extension. +- Every `ref="prefix:name"` must resolve to an in-scope element declaration. +- Every `type="prefix:TypeName"` must resolve to an in-scope type definition. + +## Custom Extensions +If SAP's XSD is missing types: +1. Create a custom extension XSD in `.xsd/custom/` with the **SAME** targetNamespace as the SAP schema. +2. Use `xs:include` to bring in the SAP schema. +3. Add new types/elements in the extension. +4. Have consumers `xs:import` the extension instead of the SAP schema directly. + +## XML Parsing Gotchas +- **xmldom & Attributes:** The `@xmldom/xmldom` library returns an empty string `""` (instead of `null`) when calling `getAttribute()` for a non-existent attribute. + - **Rule:** Always use `hasAttribute()` to check for existence before reading values to avoid treating missing attributes as present empty strings. + +## TS-XSD Builder Logic (Do Not Regress) +- **Qualified Attributes:** When building XML with schemas that use `attributeFormDefault="qualified"` and inherit attributes from imported schemas, the builder must collect namespace prefixes from the **ENTIRE** `$imports` chain, not just the root schema's `$xmlns`. + - *Context:* SAP ADT schemas often import `abapsource.xsd` -> `adtcore.xsd`. Attributes like `name` defined in `adtcore.xsd` need the `adtcore:` prefix even if the root schema doesn't import `adtcore` directly. +- **Inherited Child Elements:** When building XML with inherited child elements (e.g. `packageRef` defined in `adtcore.xsd` but used by `interfaces.xsd`), the builder must use the **defining schema's namespace prefix** for the element tag, not the root schema's prefix. + - *Context:* `walkElements` must return the schema where the element was defined. `buildElement` must use this schema to resolve the prefix. diff --git a/.agents/rules/development/coding-conventions.md b/.agents/rules/development/coding-conventions.md new file mode 100644 index 00000000..23a69c51 --- /dev/null +++ b/.agents/rules/development/coding-conventions.md @@ -0,0 +1,16 @@ +--- +trigger: always_on +description: Core coding conventions for the abapify monorepo. TypeScript strict, ESM only, naming, formatting. +--- + +# Coding Conventions + +- **TypeScript strict** — no `any` without a comment explaining why +- **ESM only** — no `require()`, no `__dirname` (use `import.meta.url`) +- **No decorators** except in packages that already use them +- **Async/await** over Promises `.then()` chains +- PascalCase for types/classes/interfaces; camelCase for variables/functions +- 2-space indentation (Prettier enforced) +- Cross-package imports: `@abapify/` +- Internal file imports: relative paths, no extension (`../utils/parse`) +- `workspace:*` protocol for local workspace deps diff --git a/.agents/rules/development/file-lifecycle.md b/.agents/rules/development/file-lifecycle.md new file mode 100644 index 00000000..06e89c17 --- /dev/null +++ b/.agents/rules/development/file-lifecycle.md @@ -0,0 +1,21 @@ +--- +trigger: always_on +description: Know which files are generated/downloaded before editing. Never edit codegen output or SAP XSD downloads. +--- + +# File Lifecycle — Know Before You Edit + +**Before editing ANY file**, check whether it's generated/downloaded: + +```bash +bunx nx show project --json | grep -i "xsd\|generated\|download" +``` + +| Pattern | Lifecycle | Rule | +| ------------------------------------- | ------------------- | ------------------------------------------------------ | +| `packages/*/src/schemas/generated/**` | Codegen output | Never edit — fix the generator or XSD source | +| `packages/adt-schemas/.xsd/sap/**` | Downloaded from SAP | Never edit — create custom extension in `.xsd/custom/` | +| `packages/adt-schemas/.xsd/custom/**` | Hand-maintained | Safe to edit | +| `packages/*/dist/**` | Build output | Never edit | + +If an edit keeps "reverting": **stop**. Something is regenerating the file. Check Nx targets before using `sed`/force-writes. diff --git a/.agents/rules/development/tooling/nx-circular-dependencies.md b/.agents/rules/nx/nx-circular-dependencies.md similarity index 100% rename from .agents/rules/development/tooling/nx-circular-dependencies.md rename to .agents/rules/nx/nx-circular-dependencies.md diff --git a/.agents/rules/development/tooling/nx-monorepo-setup.md b/.agents/rules/nx/nx-monorepo-setup.md similarity index 100% rename from .agents/rules/development/tooling/nx-monorepo-setup.md rename to .agents/rules/nx/nx-monorepo-setup.md diff --git a/.agents/rules/planning/project-planning-memory.md b/.agents/rules/openspec/project-planning-memory.md similarity index 100% rename from .agents/rules/planning/project-planning-memory.md rename to .agents/rules/openspec/project-planning-memory.md diff --git a/.agents/rules/planning/spec-first-then-code.md b/.agents/rules/openspec/spec-first-then-code.md similarity index 100% rename from .agents/rules/planning/spec-first-then-code.md rename to .agents/rules/openspec/spec-first-then-code.md diff --git a/.agents/rules/verification/after-changes.md b/.agents/rules/verification/after-changes.md new file mode 100644 index 00000000..1d2031a0 --- /dev/null +++ b/.agents/rules/verification/after-changes.md @@ -0,0 +1,14 @@ +--- +trigger: always_on +description: Verification checklist after making changes. Build, typecheck, test, lint, format. +--- + +# After Making Changes + +```bash +bunx nx build # verify it compiles +bunx nx typecheck # full type check +bunx nx test # run tests +bunx nx lint # fix lint issues +bunx nx format:write # REQUIRED before every commit — format all files with Prettier +``` diff --git a/AGENTS.md b/AGENTS.md index f31cec59..9deb3fa1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,73 +19,27 @@ AI agent conventions for the **abapify / adt-cli** monorepo. ## Essential Commands ```bash -# Build -bunx nx build # all packages -bunx nx build adt-cli # single package - -# Test -bunx nx test # all packages -bunx nx test adt-cli # single package -bunx nx test adt-cli --watch # watch mode - -# Type check -bunx nx typecheck - -# Lint (auto-fix) -bunx nx lint +bunx nx build [package] # build one or all packages +bunx nx test [package] # run tests +bunx nx typecheck # full type check +bunx nx lint # lint (auto-fix) +bunx nx format:write # REQUIRED before every commit ``` ## Monorepo Layout ``` / -├── packages/ -│ ├── adt-cli/ # CLI binary (@abapify/adt-cli) -│ ├── adt-client/ # HTTP client (@abapify/adt-client) -│ ├── adt-contracts/ # API contracts (@abapify/adt-contracts) -│ ├── adt-schemas/ # XSD-derived schemas (@abapify/adt-schemas) -│ ├── adk/ # ABAP object kit (@abapify/adk) -│ ├── adt-auth/ # Auth methods (@abapify/adt-auth) -│ ├── adt-config/ # Config loader (@abapify/adt-config) -│ ├── adt-atc/ # ATC plugin (@abapify/adt-atc) -│ ├── adt-export/ # Export plugin (@abapify/adt-export) -│ ├── adt-plugin/ # Plugin interface (@abapify/adt-plugin) -│ ├── adt-plugin-abapgit/ # abapGit plugin (@abapify/adt-plugin-abapgit) -│ ├── browser-auth/ # Browser SSO core (@abapify/browser-auth) -│ ├── adt-playwright/ # Playwright adapter (@abapify/adt-playwright) -│ ├── adt-puppeteer/ # Puppeteer adapter (@abapify/adt-puppeteer) -│ ├── speci/ # Contract spec (@abapify/speci) -│ ├── ts-xsd/ # XSD tools (@abapify/ts-xsd) -│ ├── adt-codegen/ # Code gen (@abapify/adt-codegen) -│ ├── asjson-parser/ # asJSON parser (@abapify/asjson-parser) -│ └── logger/ # Logger (@abapify/logger) -├── samples/ # Example projects -├── tools/ # Nx plugins/tools -├── openspec/ # OpenSpec specs and changes (source of truth) -│ ├── specs/ # Domain specifications (adk, adt-cli, cicd, etc.) -│ ├── changes/ # Proposed changes (one folder per change) -│ └── config.yaml # OpenSpec project configuration -├── docs/ # Architecture docs, migration notes, examples -└── tmp/ # Local scratch files (gitignored) +├── packages/ # All publishable packages (@abapify/*) +├── samples/ # Example projects +├── tools/ # Nx plugins/tools +├── openspec/ # Specs and changes (source of truth) +├── .agents/rules/ # AI agent rules (symlinked to .windsurf/rules/ and .cognition/rules/) +├── docs/ # Architecture docs +└── tmp/ # Scratch files (gitignored) ``` -## Package Naming Rules - -- Cross-package imports: `@abapify/` -- Internal file imports: relative paths, no extension (`../utils/parse`) -- `workspace:*` protocol is supported by bun — use it for local deps - -## Creating a New Package - -```bash -bunx nx g @nx/node:library --directory=packages/ --no-interactive -# then copy tsdown.config.ts from packages/sample-tsdown -# set "build": "tsdown" in package.json -``` - -Ensure `skipNodeModulesBundle: true` in `tsdown.config.ts`. - -## Dependency Graph (simplified) +## Dependency Graph ``` adt-cli @@ -97,11 +51,9 @@ adt-cli └── adt-plugin (interface) ``` -Foundation packages with no `@abapify` dependencies: `@abapify/ts-xsd`, `@abapify/speci`, `@abapify/logger`. +Foundation packages (no `@abapify` deps): `ts-xsd`, `speci`, `logger`. -## Key Architectural Decisions - -### Type Flow +## Type Flow (Core Architecture) ``` SAP XSD files @@ -111,136 +63,48 @@ SAP XSD files → adt-client (executes contracts, full type inference at call site) ``` -### Plugin System - -CLI plugins are loaded from `adt.config.ts` → `commands` array. Each plugin exports a Commander.js `Command` object. Plugins must depend on `@abapify/adt-plugin` for the format interface. - -### ADK Object Handlers - -The ADK uses an `AdkObjectHandler` bridge pattern. Object types are registered with a parser function and a URL factory: - -```typescript -this.handlers.set( - 'CLAS', - (client) => - new AdkObjectHandler( - client, - (xml) => ClassAdtAdapter.fromAdtXML(xml), - (name) => `/sap/bc/adt/oo/classes/${name.toLowerCase()}`, - ), -); -``` - -### ADK Save Lifecycle - -`AdkObject.save()` has a pre-lock comparison phase for source-based objects: - -``` -save(options) - ├── hasPendingSources()? - │ ├── YES → checkPendingSourcesUnchanged() ← pre-lock GET+compare - │ │ ├── all identical → _unchanged=true, return early (no lock, no PUT) - │ │ └── at least one differs → continue to lock+save - │ └── NO → saveViaContract() (metadata PUT) - ├── lock() - ├── savePendingSources() or saveViaContract() - └── unlock() -``` - -**Object types with source code** (CLAS, INTF, PROG, FUGR) override `savePendingSources()`. -All four also override `checkPendingSourcesUnchanged()` for skip-unchanged support. -New source-based object types should implement both. - -`AdkObjectSet.deploy()` orchestrates two-phase deployment: - -1. `saveAll({ inactive: true })` — create inactive versions -2. `activateAll()` — bulk activate (skipped if `activate: false`) - -`BulkSaveResult` tracks `success`, `failed`, and `unchanged` counts. - -### Export / Plugin Contract - -The export command (`adt-export`) is **format-agnostic**. Format plugins receive: - -```typescript -export?(fileTree: FileTree, client: AdtClient, options?: ExportOptions): AsyncGenerator -``` - -`ExportOptions` provides deployment context (`rootPackage`, `abapLanguageVersion`). -Format-specific concerns (abapGit folder logic, package resolution) live **in the plugin**, not the export command. - -### Auth Flow - -1. CLI reads destination from `adt-config` -2. `adt-auth` `AuthManager` picks the matching auth method -3. For browser SSO: delegates to `adt-playwright` or `adt-puppeteer` (loaded as plugins) -4. Session cached in `~/.adt/sessions/.json` - -## Coding Conventions - -- **TypeScript strict** — no `any` without a comment explaining why -- **ESM only** — no `require()`, no `__dirname` (use `import.meta.url`) -- **No decorators** except in packages that already use them -- **Async/await** over Promises `.then()` chains -- PascalCase for types/classes/interfaces; camelCase for variables/functions -- 2-space indentation (Prettier enforced) - -## Temporary Files - -Always write scratch files, test output, and CLI output to `tmp/`: - -```bash -adt get ZCL_TEST -o tmp/class.xml -``` - -The `tmp/` directory is gitignored. - -## Specification-Driven Development (OpenSpec) - -This project uses [OpenSpec](https://openspec.dev/) for spec-driven development. - -Specifications live in `openspec/specs/` organized by domain: - -- `openspec/specs/adk/` — ABAP Development Kit -- `openspec/specs/adt-cli/` — CLI design and plugin architecture -- `openspec/specs/adt-client/` — ADT API client -- `openspec/specs/cicd/` — CI/CD pipeline architecture - -### Before implementing a new feature: - -1. Check `openspec/specs/` for an existing spec -2. If one exists, align implementation to it -3. To propose a new change, use `/opsx:propose "description"` -4. Review and apply changes with `/opsx:apply`, then archive with `/opsx:archive` - -See `openspec/config.yaml` for project context and per-artifact rules. - -## File Lifecycle — Know Before You Edit - -**Before editing ANY file**, check whether it's generated/downloaded: - -```bash -# Check if a file path appears in any Nx target outputs -bunx nx show project --json | grep -i "xsd\|generated\|download" -``` - -| Pattern | Lifecycle | Rule | -| ------------------------------------- | ------------------- | ------------------------------------------------------ | -| `packages/*/src/schemas/generated/**` | Codegen output | Never edit — fix the generator or XSD source | -| `packages/adt-schemas/.xsd/sap/**` | Downloaded from SAP | Never edit — create custom extension in `.xsd/custom/` | -| `packages/adt-schemas/.xsd/custom/**` | Hand-maintained | Safe to edit | -| `packages/*/dist/**` | Build output | Never edit | - -If an edit keeps "reverting": **stop**. Something is regenerating the file. Check Nx targets before using `sed`/force-writes. - -## After Making Changes - -```bash -bunx nx build # verify it compiles -bunx nx typecheck # full type check -bunx nx test # run tests -bunx nx lint # fix lint issues -bunx nx format:write # REQUIRED before every commit — format all files with Prettier -``` +## Rules Index + +All AI agent rules live in `.agents/rules/` (single source of truth). +Symlinked to `.windsurf/rules/` and `.cognition/rules/` for tool compatibility. + +### Always On +| Rule | Description | +| ---- | ----------- | +| [`git/no-auto-commit`](.agents/rules/git/no-auto-commit.md) | Never commit or push without explicit user approval | +| [`development/coding-conventions`](.agents/rules/development/coding-conventions.md) | TS strict, ESM only, naming, formatting, import conventions | +| [`development/file-lifecycle`](.agents/rules/development/file-lifecycle.md) | Generated/downloaded file guardrails | +| [`planning/project-planning-memory`](.agents/rules/planning/project-planning-memory.md) | OpenSpec workflow and project memory | +| [`verification/after-changes`](.agents/rules/verification/after-changes.md) | Build, typecheck, test, lint, format checklist | + +### On Demand (model_decision) +| Rule | Description | +| ---- | ----------- | +| [`planning/spec-first-then-code`](.agents/rules/planning/spec-first-then-code.md) | Check specs before coding | +| [`development/tmp-folder-testing`](.agents/rules/development/tmp-folder-testing.md) | Use `tmp/` for scratch files | +| [`development/package-versions`](.agents/rules/development/package-versions.md) | Always install latest versions | +| [`adt/adk-save-logic`](.agents/rules/adt/adk-save-logic.md) | ADK upsert/lock edge cases | +| [`adt/adt-ddic-mapping`](.agents/rules/adt/adt-ddic-mapping.md) | DDIC object → schema mapping | +| [`adt/xsd-best-practices`](.agents/rules/adt/xsd-best-practices.md) | XSD validity and builder rules | +| [`development/tooling/nx-monorepo-setup`](.agents/rules/development/tooling/nx-monorepo-setup.md) | Package creation, config templates | +| [`development/tooling/nx-circular-dependencies`](.agents/rules/development/tooling/nx-circular-dependencies.md) | Fix false circular dep issues | + +### File-Scoped (glob) +| Rule | Glob | Description | +| ---- | ---- | ----------- | +| [`development/bundler-imports`](.agents/rules/development/bundler-imports.md) | `**/*.ts` | Extensionless imports for bundled packages | + +## Package-Level Guides + +Each package has its own `AGENTS.md` with detailed conventions: + +- [`packages/adt-cli/AGENTS.md`](packages/adt-cli/AGENTS.md) — CLI commands, service pattern, client initialization +- [`packages/adt-client/AGENTS.md`](packages/adt-client/AGENTS.md) — Contract-driven REST client, schema conventions, type inference +- [`packages/adt-contracts/AGENTS.md`](packages/adt-contracts/AGENTS.md) — Contract testing framework, schema integration +- [`packages/adt-schemas/AGENTS.md`](packages/adt-schemas/AGENTS.md) — XSD-derived schemas, generation pipeline +- [`packages/adt-plugin-abapgit/AGENTS.md`](packages/adt-plugin-abapgit/AGENTS.md) — abapGit serialization, handler template +- [`packages/ts-xsd/AGENTS.md`](packages/ts-xsd/AGENTS.md) — W3C XSD parser, type inference, codegen +- [`packages/adt-auth/AGENTS.md`](packages/adt-auth/AGENTS.md) — Auth methods, browser SSO +- [`packages/adt-fixtures/AGENTS.md`](packages/adt-fixtures/AGENTS.md) — Test fixtures diff --git a/git_modules/abapgit-examples b/git_modules/abapgit-examples index aaa1d729..76e091bb 160000 --- a/git_modules/abapgit-examples +++ b/git_modules/abapgit-examples @@ -1 +1 @@ -Subproject commit aaa1d7297a93e2dbafb4b39441bd355f27f61ae3 +Subproject commit 76e091bb07cd0e6cd9cfb4c3981615fbddd1b06c From 877475c1f20d17f6f70184836c44c2901ad7ccf2 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 18 Mar 2026 10:19:15 +0100 Subject: [PATCH 18/19] docs: cross-link agent rules and skills, standardize tool references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add cross-references between related rules (coding-conventions ↔ bundler-imports, project-planning-memory ↔ spec-first-then-code, no-auto-commit ↔ monitor-ci exception). Update skills to reference verification/after-changes checklist and relevant rules (xsd-best-practices, file-lifecycle, adt-ddic-mapping, adk-save-logic, tmp-folder-testing). Replace npx/pnpm with bunx in skills (add-endpoint, add-object-type, nx-generate --- .../rules/development/coding-conventions.md | 4 +- .agents/rules/git/no-auto-commit.md | 10 +++++ .agents/rules/nx/nx-monorepo-setup.md | 10 ++--- .../rules/openspec/project-planning-memory.md | 2 + .../rules/openspec/spec-first-then-code.md | 2 + .agents/skills/add-endpoint/SKILL.md | 35 ++++++++--------- .agents/skills/add-object-type/SKILL.md | 39 ++++++++----------- .../skills/adt-reverse-engineering/SKILL.md | 4 +- .agents/skills/monitor-ci/SKILL.md | 2 +- .agents/skills/nx-generate/SKILL.md | 2 + .agents/skills/nx-plugins/SKILL.md | 6 ++- AGENTS.md | 8 ++-- 12 files changed, 68 insertions(+), 56 deletions(-) diff --git a/.agents/rules/development/coding-conventions.md b/.agents/rules/development/coding-conventions.md index 23a69c51..1f8d1933 100644 --- a/.agents/rules/development/coding-conventions.md +++ b/.agents/rules/development/coding-conventions.md @@ -12,5 +12,5 @@ description: Core coding conventions for the abapify monorepo. TypeScript strict - PascalCase for types/classes/interfaces; camelCase for variables/functions - 2-space indentation (Prettier enforced) - Cross-package imports: `@abapify/` -- Internal file imports: relative paths, no extension (`../utils/parse`) -- `workspace:*` protocol for local workspace deps +- Internal file imports: extensionless relative paths — see [bundler-imports](bundler-imports.md) for details +- `workspace:*` protocol for local workspace deps — see `$link-workspace-packages` skill for setup diff --git a/.agents/rules/git/no-auto-commit.md b/.agents/rules/git/no-auto-commit.md index 24e6e78c..4242cfa3 100644 --- a/.agents/rules/git/no-auto-commit.md +++ b/.agents/rules/git/no-auto-commit.md @@ -15,6 +15,16 @@ Before committing: 2. Wait for the user to confirm 3. Only then execute `git commit` +## Exception: `$monitor-ci` Skill + +When the `$monitor-ci` skill is active (CI self-healing workflow), autonomous commits and pushes are permitted for: + +- Applying self-healing fixes +- Retrying CI with empty commits +- Updating lockfiles for pre-CI failures + +The skill has its own git safety rules (never `git add -A`, stage only fix-related files). + ## Applies To - All AI assistants (Devin, Windsurf, Claude, etc.) diff --git a/.agents/rules/nx/nx-monorepo-setup.md b/.agents/rules/nx/nx-monorepo-setup.md index b5e4d11a..45cdf75d 100644 --- a/.agents/rules/nx/nx-monorepo-setup.md +++ b/.agents/rules/nx/nx-monorepo-setup.md @@ -5,8 +5,6 @@ description: Nx monorepo setup rules - package creation workflow, config templat # Nx Monorepo Setup Rules -> **See**: [`.agents/rules/development/tooling/nx-monorepo.md`](../../../.agents/rules/development/tooling/nx-monorepo.md) for complete Nx plugin inference system documentation. - ## Quick Reference ### Core Commands @@ -158,13 +156,15 @@ You should see: `["build", "lint", "nx-release-publish", "test", "test:coverage" ## Import Rules +> See [coding-conventions](../development/coding-conventions.md) and [bundler-imports](../development/bundler-imports.md) for full details. + - **Cross-package**: `@abapify/[package-name]` -- **Internal files**: `../relative/path` (no extensions for TS files) -- **Workspace deps**: Use `workspace:*` (bun supports this protocol) +- **Internal files**: `../relative/path` (extensionless) +- **Workspace deps**: `workspace:*` ## File Organization - **Source code**: `packages/[name]/src/` -- **Temporary files**: `tmp/` (never commit) +- **Temporary files**: `tmp/` — see [tmp-folder-testing](../development/tmp-folder-testing.md) - **Build output**: `packages/[name]/dist/` - **Tests**: Co-located with source (`*.test.ts`) diff --git a/.agents/rules/openspec/project-planning-memory.md b/.agents/rules/openspec/project-planning-memory.md index 99dc580c..17a191d8 100644 --- a/.agents/rules/openspec/project-planning-memory.md +++ b/.agents/rules/openspec/project-planning-memory.md @@ -18,6 +18,8 @@ This workspace uses [OpenSpec](https://openspec.dev/) for project planning and s ## AI Assistant Workflow +> For the critical review process and spec change severity rules, see [spec-first-then-code](spec-first-then-code.md). + 1. **Check `openspec/specs/`** for existing specifications before making changes 2. **Check `openspec/changes/`** for active change proposals to understand context 3. **Use `/opsx:propose`** to create new change proposals with specs, design, and tasks diff --git a/.agents/rules/openspec/spec-first-then-code.md b/.agents/rules/openspec/spec-first-then-code.md index 0e854699..05fb5aea 100644 --- a/.agents/rules/openspec/spec-first-then-code.md +++ b/.agents/rules/openspec/spec-first-then-code.md @@ -7,6 +7,8 @@ description: When making any changes for any of spec-driven packages ## Core Philosophy +> For key locations and memory persistence, see [project-planning-memory](project-planning-memory.md). + - Specifications are design contracts (stable, versioned, change-resistant) - Documentation describes implementation (living, refactorable) - Specs define WHAT and WHY before coding HOW diff --git a/.agents/skills/add-endpoint/SKILL.md b/.agents/skills/add-endpoint/SKILL.md index b4ec2c34..d70b11e1 100644 --- a/.agents/skills/add-endpoint/SKILL.md +++ b/.agents/skills/add-endpoint/SKILL.md @@ -43,7 +43,7 @@ If you have a running SAP system connection: # Example using curl or adt-cli: # GET /sap/bc/adt/{path}/{objectname} ``` -2. Save the raw response to `tmp/` for reference: +2. Save the raw response to `tmp/` for reference (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)): ```bash # adt get ZOBJECT_NAME -o tmp/response.xml ``` @@ -141,12 +141,14 @@ export default { **Key rules for schema literals:** +> See [xsd-best-practices](../../rules/adt/xsd-best-practices.md) for XSD validity rules and edge cases. + - Must end with `} as const` - One root element per schema document - Use `$imports` for schemas you depend on (adtcore, abapoo, abapsource, etc.) - Names follow XSD conventions: `complexType` names are PascalCase - Look at existing schemas (e.g. `interfaces.ts`, `packagesV1.ts`) for common base types -- Existing schemas were auto-generated from XSD sources in `packages/adt-schemas/.xsd/`; manually created schemas should be documented accordingly +- Existing schemas were auto-generated from XSD sources in `packages/adt-schemas/.xsd/`; manually created schemas should be documented accordingly (see [file-lifecycle](../../rules/development/file-lifecycle.md)) ### 2b: Add the typed wrapper in typed.ts @@ -370,21 +372,18 @@ export const registry = { ## Step 6: Build and Verify +> Follow the verification checklist in [after-changes](../../rules/verification/after-changes.md). + ```bash # Build affected packages in dependency order -npx nx build adt-schemas -npx nx build adt-contracts -npx nx build adt-fixtures - -# Type check everything -npx nx typecheck - -# Run tests -npx nx test adt-schemas -npx nx test adt-contracts - -# Lint -npx nx lint +bunx nx build adt-schemas +bunx nx build adt-contracts +bunx nx build adt-fixtures + +# Type check, test, lint +bunx nx typecheck +bunx nx test adt-schemas adt-contracts +bunx nx lint ``` ### Quick smoke test (optional) @@ -467,6 +466,6 @@ application/vnd.sap.adt.{object-type}.v{version}+xml - [ ] Module index updated (or new module registered in main `adt/index.ts`) - [ ] Fixture XML created in `adt-fixtures/src/fixtures/{module}/` - [ ] Fixture registered in `adt-fixtures/src/fixtures/registry.ts` -- [ ] `npx nx build adt-schemas adt-contracts adt-fixtures` passes -- [ ] `npx nx typecheck` passes -- [ ] `npx nx test adt-schemas adt-contracts` passes +- [ ] `bunx nx build adt-schemas adt-contracts adt-fixtures` passes +- [ ] `bunx nx typecheck` passes +- [ ] `bunx nx test adt-schemas adt-contracts` passes diff --git a/.agents/skills/add-object-type/SKILL.md b/.agents/skills/add-object-type/SKILL.md index 877e102e..ddd66acf 100644 --- a/.agents/skills/add-object-type/SKILL.md +++ b/.agents/skills/add-object-type/SKILL.md @@ -75,7 +75,9 @@ Before writing code, gather details about the object type from live system or re ## Step 2: Create the ADT Schema and Contract -Follow the full **add-endpoint** skill for this step. Here is a summary specific to object types: +Follow the full **$add-endpoint** skill for this step. Here is a summary specific to object types: + +> **Rules:** See [xsd-best-practices](../../rules/adt/xsd-best-practices.md) for XSD validity, [file-lifecycle](../../rules/development/file-lifecycle.md) for generated vs hand-maintained files, and [adt-ddic-mapping](../../rules/adt/adt-ddic-mapping.md) for DDIC object specifics. ### Schema (adt-schemas) @@ -353,6 +355,8 @@ registerObjectType('PROG', ProgramKind, AdkProgram); **IMPORTANT:** Both `savePendingSources()` AND `checkPendingSourcesUnchanged()` must be implemented. +> See [adk-save-logic](../../rules/adt/adk-save-logic.md) for edge cases around upsert, lock failures (405), and 422 "already exists" handling. + - `checkPendingSourcesUnchanged()` runs **before** lock — compares pending source with SAP, sets `_unchanged = true` if identical - `savePendingSources()` runs **after** lock — does the actual PUT - Without `checkPendingSourcesUnchanged()`, unchanged objects will still be locked and PUT unnecessarily @@ -780,25 +784,16 @@ export { AdkProgram } from '@abapify/adk'; ## Step 8: Build and Verify +> Follow the verification checklist in [after-changes](../../rules/verification/after-changes.md). + ```bash # Build all affected packages in dependency order -npx nx build adt-schemas -npx nx build adt-contracts -npx nx build adk -npx nx build adt-plugin-abapgit -npx nx build adt-fixtures - -# Full type check -npx nx typecheck - -# Run tests -npx nx test adt-schemas -npx nx test adt-contracts -npx nx test adk -npx nx test adt-plugin-abapgit - -# Lint -npx nx lint +bunx nx build adt-schemas adt-contracts adk adt-plugin-abapgit adt-fixtures + +# Full type check, tests, lint +bunx nx typecheck +bunx nx test adt-schemas adt-contracts adk adt-plugin-abapgit +bunx nx lint ``` ### Quick smoke tests @@ -879,7 +874,7 @@ expect(handler).toBeDefined(); ### Verification -- [ ] `npx nx build adt-schemas adt-contracts adk adt-plugin-abapgit adt-fixtures` passes -- [ ] `npx nx typecheck` passes -- [ ] `npx nx test adt-schemas adt-contracts adk adt-plugin-abapgit` passes -- [ ] `npx nx lint` passes +- [ ] `bunx nx build adt-schemas adt-contracts adk adt-plugin-abapgit adt-fixtures` passes +- [ ] `bunx nx typecheck` passes +- [ ] `bunx nx test adt-schemas adt-contracts adk adt-plugin-abapgit` passes +- [ ] `bunx nx lint` passes diff --git a/.agents/skills/adt-reverse-engineering/SKILL.md b/.agents/skills/adt-reverse-engineering/SKILL.md index c495cea6..7d92e1ef 100644 --- a/.agents/skills/adt-reverse-engineering/SKILL.md +++ b/.agents/skills/adt-reverse-engineering/SKILL.md @@ -99,7 +99,7 @@ curl -u "$SAP_USER:$SAP_PASSWORD" \ -v 2>tmp/headers.txt >tmp/response.xml ``` -**Save ALL raw responses to `tmp/`** — they become fixtures later. +**Save ALL raw responses to `tmp/`** (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)) — they become fixtures later. --- @@ -303,7 +303,7 @@ grep -r 'xmlns.*sap.com/adt' packages/adt-fixtures/ ## Step 5: Analyze and Document Findings -After gathering data from the sources above, create a structured analysis in `tmp/`. +After gathering data from the sources above, create a structured analysis in `tmp/` (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)). ### 5a: Create endpoint analysis file diff --git a/.agents/skills/monitor-ci/SKILL.md b/.agents/skills/monitor-ci/SKILL.md index 4027df13..df1c2a57 100644 --- a/.agents/skills/monitor-ci/SKILL.md +++ b/.agents/skills/monitor-ci/SKILL.md @@ -177,7 +177,7 @@ Failed tasks: , Local verification: passed|enhanced|failed-pushing-to-ci" ``` -**Git Safety**: Only stage and commit files that were modified as part of the fix. Users may have concurrent local changes (local publish, WIP features, config tweaks) that must NOT be committed. NEVER use `git add -A` or `git add .` — always stage specific files by name. +**Git Safety**: Only stage and commit files that were modified as part of the fix. Users may have concurrent local changes (local publish, WIP features, config tweaks) that must NOT be committed. NEVER use `git add -A` or `git add .` — always stage specific files by name. (This skill is exempt from [no-auto-commit](../../rules/git/no-auto-commit.md) for self-healing commits only.) ### Unverified Fix Flow (No Verification Attempted) diff --git a/.agents/skills/nx-generate/SKILL.md b/.agents/skills/nx-generate/SKILL.md index af7ba80a..7c079161 100644 --- a/.agents/skills/nx-generate/SKILL.md +++ b/.agents/skills/nx-generate/SKILL.md @@ -146,6 +146,8 @@ Generators provide a starting point. Modify the output as needed to: ### 9. Format and Verify +> See [after-changes](../../rules/verification/after-changes.md) for this project's verification commands. + Format all generated/modified files: ```bash diff --git a/.agents/skills/nx-plugins/SKILL.md b/.agents/skills/nx-plugins/SKILL.md index 89223c7f..80a0f5fb 100644 --- a/.agents/skills/nx-plugins/SKILL.md +++ b/.agents/skills/nx-plugins/SKILL.md @@ -5,5 +5,7 @@ description: Find and add Nx plugins. USE WHEN user wants to discover available ## Finding and Installing new plugins -- List plugins: `pnpm nx list` -- Install plugins `pnpm nx add `. Example: `pnpm nx add @nx/react`. +- List plugins: `bunx nx list` +- Install plugins `bunx nx add `. Example: `bunx nx add @nx/react`. + +> **Note:** This project uses **bun** as its package manager. See [coding-conventions](../../rules/development/coding-conventions.md) for import and naming rules. diff --git a/AGENTS.md b/AGENTS.md index 9deb3fa1..86963a87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,20 +74,20 @@ Symlinked to `.windsurf/rules/` and `.cognition/rules/` for tool compatibility. | [`git/no-auto-commit`](.agents/rules/git/no-auto-commit.md) | Never commit or push without explicit user approval | | [`development/coding-conventions`](.agents/rules/development/coding-conventions.md) | TS strict, ESM only, naming, formatting, import conventions | | [`development/file-lifecycle`](.agents/rules/development/file-lifecycle.md) | Generated/downloaded file guardrails | -| [`planning/project-planning-memory`](.agents/rules/planning/project-planning-memory.md) | OpenSpec workflow and project memory | +| [`openspec/project-planning-memory`](.agents/rules/openspec/project-planning-memory.md) | OpenSpec workflow and project memory | | [`verification/after-changes`](.agents/rules/verification/after-changes.md) | Build, typecheck, test, lint, format checklist | ### On Demand (model_decision) | Rule | Description | | ---- | ----------- | -| [`planning/spec-first-then-code`](.agents/rules/planning/spec-first-then-code.md) | Check specs before coding | +| [`openspec/spec-first-then-code`](.agents/rules/openspec/spec-first-then-code.md) | Check specs before coding | | [`development/tmp-folder-testing`](.agents/rules/development/tmp-folder-testing.md) | Use `tmp/` for scratch files | | [`development/package-versions`](.agents/rules/development/package-versions.md) | Always install latest versions | | [`adt/adk-save-logic`](.agents/rules/adt/adk-save-logic.md) | ADK upsert/lock edge cases | | [`adt/adt-ddic-mapping`](.agents/rules/adt/adt-ddic-mapping.md) | DDIC object → schema mapping | | [`adt/xsd-best-practices`](.agents/rules/adt/xsd-best-practices.md) | XSD validity and builder rules | -| [`development/tooling/nx-monorepo-setup`](.agents/rules/development/tooling/nx-monorepo-setup.md) | Package creation, config templates | -| [`development/tooling/nx-circular-dependencies`](.agents/rules/development/tooling/nx-circular-dependencies.md) | Fix false circular dep issues | +| [`nx/nx-monorepo-setup`](.agents/rules/nx/nx-monorepo-setup.md) | Package creation, config templates | +| [`nx/nx-circular-dependencies`](.agents/rules/nx/nx-circular-dependencies.md) | Fix false circular dep issues | ### File-Scoped (glob) | Rule | Glob | Description | From 07a59dab44f1c670c7d4c1ecc20d684a0226e3bc Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Thu, 19 Mar 2026 00:03:44 +0100 Subject: [PATCH 19/19] feat(adt-diff): rename --format to --source, add annotation filtering for CDS comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename `--format ddl` to `--source` flag (boolean) for clarity. When comparing CDS source, filter remote annotations to match local's annotation set — prevents spurious diffs from fields present in remote XML but absent in local (e.g. MATEFLAG). Also add MATEFLAG support in cds-to-abapgit (maps AbapCatalog.dataMaintenance annotation) and strengthen after-changes rule to require agents verify builds/tests --- .agents/rules/verification/after-changes.md | 31 +++++---- .cognition/skills | 1 + nul | 3 + packages/adt-diff/src/commands/diff.ts | 65 +++++++++++-------- .../src/lib/handlers/cds-to-abapgit.ts | 9 ++- 5 files changed, 67 insertions(+), 42 deletions(-) create mode 120000 .cognition/skills create mode 100644 nul diff --git a/.agents/rules/verification/after-changes.md b/.agents/rules/verification/after-changes.md index 1d2031a0..a9b90b0e 100644 --- a/.agents/rules/verification/after-changes.md +++ b/.agents/rules/verification/after-changes.md @@ -1,14 +1,17 @@ ---- -trigger: always_on -description: Verification checklist after making changes. Build, typecheck, test, lint, format. ---- - -# After Making Changes - -```bash -bunx nx build # verify it compiles -bunx nx typecheck # full type check -bunx nx test # run tests -bunx nx lint # fix lint issues -bunx nx format:write # REQUIRED before every commit — format all files with Prettier -``` +--- +trigger: always_on +description: Verification checklist after making changes. Build, typecheck, test, lint, format. +--- + +# After Making Changes + +**NEVER tell the user to "try it" or "run it again" — verify it yourself first.** +If you changed code, YOU must build and test before declaring it done. + +```bash +bunx nx build # verify it compiles +bunx nx typecheck # full type check +bunx nx test # run tests +bunx nx lint # fix lint issues +bunx nx format:write # REQUIRED before every commit — format all files with Prettier +``` diff --git a/.cognition/skills b/.cognition/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.cognition/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/nul b/nul new file mode 100644 index 00000000..03e1b18c --- /dev/null +++ b/nul @@ -0,0 +1,3 @@ +dir: cannot access '/s': No such file or directory +dir: cannot access '/b': No such file or directory +dir: cannot access '%USERPROFILE%\.config\cognition\*': No such file or directory diff --git a/packages/adt-diff/src/commands/diff.ts b/packages/adt-diff/src/commands/diff.ts index a74b4899..2f361171 100644 --- a/packages/adt-diff/src/commands/diff.ts +++ b/packages/adt-diff/src/commands/diff.ts @@ -262,10 +262,10 @@ async function diffSingleFile( options: { contextLines: number; useColor: boolean; - format: string; + source: boolean; }, ): Promise { - const { contextLines, useColor, format } = options; + const { contextLines, useColor, source } = options; const fullPath = resolve(ctx.cwd, filePath); if (!existsSync(fullPath)) { @@ -304,15 +304,15 @@ async function diffSingleFile( }; } - // Validate --format option - if (format === 'ddl' && parsed.type !== 'TABL') { + // Validate --source option + if (source && parsed.type !== 'TABL') { return { objectName: parsed.name, objectType: parsed.type, hasDifferences: false, fileCount: 0, identicalCount: 0, - error: `DDL format is only supported for TABL objects. Got: ${parsed.type}`, + error: `Source format is only supported for TABL objects. Got: ${parsed.type}`, }; } @@ -380,14 +380,14 @@ async function diffSingleFile( } // ======================================== - // DDL format: compare CDS DDL source + // Source format: compare ADK source (e.g. CDS DDL) // ======================================== - if (format === 'ddl') { - const localDdl = tablXmlToCdsDdl(localXml); + if (source) { + const localSource = tablXmlToCdsDdl(localXml); - let remoteDdl: string; + let remoteSource: string; try { - remoteDdl = await remoteObj.getSource(); + remoteSource = await remoteObj.getSource(); } catch (error) { return { objectName: parsed.name, @@ -395,16 +395,34 @@ async function diffSingleFile( hasDifferences: false, fileCount: 1, identicalCount: 0, - error: `Failed to fetch remote DDL source: ${error instanceof Error ? error.message : String(error)}`, + error: `Failed to fetch remote source: ${error instanceof Error ? error.message : String(error)}`, }; } - const ddlFile = `${objectName}.tabl.acds`; + // Project remote onto local's annotation set — same principle as XML path. + // If local XML doesn't have a field (e.g. MATEFLAG), strip the + // corresponding annotation from remote so it doesn't appear as a diff. + const localAnnotations = new Set( + localSource + .split('\n') + .filter((l) => l.startsWith('@')) + .map((l) => l.split(':')[0].trim()), + ); + remoteSource = remoteSource + .split('\n') + .filter((l) => { + if (!l.startsWith('@')) return true; + const name = l.split(':')[0].trim(); + return localAnnotations.has(name); + }) + .join('\n'); + + const sourceFile = `${objectName}.tabl.acds`; const diffFound = printDiff( - ddlFile, - ddlFile, - localDdl, - remoteDdl, + sourceFile, + sourceFile, + localSource, + remoteSource, contextLines, useColor, ); @@ -527,10 +545,9 @@ export const diffCommand: CliCommandPlugin = { default: '3', }, { - flags: '-f, --format ', + flags: '-s, --source', description: - 'Comparison format: xml (default) or ddl (TABL only — compare CDS DDL source)', - default: 'xml', + 'Compare ADK source instead of XML (TABL only)', }, ], @@ -538,7 +555,7 @@ export const diffCommand: CliCommandPlugin = { const filePatterns = (args.files as string[]) ?? []; const contextLines = parseInt(String(args.context ?? '3'), 10); const useColor = args.color !== false; - const format = String(args.format ?? 'xml').toLowerCase(); + const source = args.source === true; if (filePatterns.length === 0) { ctx.logger.error( @@ -547,12 +564,6 @@ export const diffCommand: CliCommandPlugin = { process.exit(1); } - // Validate --format option - if (format !== 'xml' && format !== 'ddl') { - ctx.logger.error(`Unknown format: ${format}. Supported: xml, ddl`); - process.exit(1); - } - // Expand glob patterns const files = await expandGlobs(filePatterns, ctx.cwd); if (files.length === 0) { @@ -576,7 +587,7 @@ export const diffCommand: CliCommandPlugin = { const result = await diffSingleFile(file, ctx, adk, { contextLines, useColor, - format, + source, }); if (result.error) { diff --git a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts index a43ca6ce..ce80cfe2 100644 --- a/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts +++ b/packages/adt-plugin-abapgit/src/lib/handlers/cds-to-abapgit.ts @@ -166,6 +166,11 @@ export function buildDD02V( 'AbapCatalog.enhancement.category', ); + const dataMaintenance = getAnnotation( + def.annotations, + 'AbapCatalog.dataMaintenance', + ); + // Detect language-dependent structure/table: // LANGDEP=X when any field references data element SPRAS or builtin type abap.lang const hasLanguageField = def.members.some((m) => { @@ -190,7 +195,7 @@ export function buildDD02V( }); // Build result in standard abapGit DD02V field order: - // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS + // TABNAME, DDLANGUAGE, TABCLASS, LANGDEP, CLIDEP, DDTEXT, MASTERLANG, CONTFLAG, EXCLASS, MATEFLAG const result: DD02VData = {}; result.TABNAME = def.name.toUpperCase(); result.DDLANGUAGE = language; @@ -202,6 +207,8 @@ export function buildDD02V( if (deliveryClass) result.CONTFLAG = deliveryClass; if (enhancementCategory) result.EXCLASS = ENHANCEMENT_CATEGORY_MAP[enhancementCategory]; + if (dataMaintenance) + result.MATEFLAG = DATA_MAINTENANCE_MAP[dataMaintenance] ?? ''; return result; }