"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateActionExecutionClass = generateActionExecutionClass;
const generator_1 = require("@postxl/generator");
function generateActionExecutionClass({ actions, database, types }) {
    const { execution } = actions;
    const { mutations, transformation, errorOperation } = execution;
    const imports = generator_1.ImportGenerator
        //
        .from(execution.class.location)
        .add(database.service)
        .addType(actions.summary.action)
        .add(actions.config)
        .addType(execution.interface)
        .addType(execution.interface.actionOperationId)
        .add(execution.interface.toBrandedActionOperationId)
        .addType(types.modelNames)
        .addType(types.user);
    return /* ts */ `
import { Injectable, Logger } from '@nestjs/common'
import * as Prisma from '${database.prismaClient.location}'

import { serializeError } from 'serialize-error'


${imports.generate()}

type DoNotInitializeFromOutside = { readonly ___type: 'DoNotInitializeFromOutside' }

const _doNotInitializeFromOutside: DoNotInitializeFromOutside = { ___type: 'DoNotInitializeFromOutside' }

@Injectable()
export class ${execution.class.name} {
  constructor(private readonly config: ${actions.config.name}){}

  /**
   * Creates an ${execution._internalName} object and initializes it.
   *
   * As the initialization is asynchronous, the ${execution._internalName} class shall not be exposed to the outside.
   * Instead, the createActionExecution function is used as a factory function.
   */
  public async create({
    action,
    user,
    databaseService,
    migrationOrder,
  }: {
    action: Action
    user: User
    databaseService: DatabaseService
    migrationOrder?: number
  }): Promise<${execution.interface.name}> {
    const execution = new ${execution._internalName}(databaseService, this.config, _doNotInitializeFromOutside)

    await execution.init({ action, user, migrationOrder })
    return execution
  }
}

type InternalState =
  | {
      type: Prisma.ActionStatus
      dbAction: DbAction
      user: User
    }
  | { type: 'NotInitialized' }

/**
 * Internal counter in case ids are in-memory only
 */
let nextActionId = 0

type DbAction = { type: string | null; scope: string | null; id: string }

type DbActionOperation = {
  id: string
  action_id: string | null
  status: 'Started' | 'Success' | 'Error'
  content_mutation_kind: DbActionOperationKind | null
}

type DbActionOperationKind =
  | 'Create'
  | 'CreateMany'
  | 'Update'
  | 'UpdateMany'
  | 'Delete'
  | 'DeleteMany'
  | 'Clone'
  | 'CloneMany'
  | 'Transformation'

/**
 * A wrapper around an Action object that enables the tracking of its execution - including all actionOperations.
 *
 * The wrapped Action object is available as the \`action\` property.
 *
 * An ${execution._internalName} can be in one of three states: started, success, error.
 *
 * While the state is "started", the ${execution._internalName} can be used to create ActionOperations.
 * Each ActionOperation represents a single operation that modifies data in the database.
 */
export class ${execution._internalName} implements ${execution.interface.name} {
  private logger = new Logger(${execution._internalName}.name)
  private readonly startTimestamp = Date.now()
  private _state: InternalState = { type: 'NotInitialized' }

  public get name(): string {
    if (this._state.type === 'NotInitialized') {
      throw new Error('${execution._internalName} is not initialized yet')
    }
    const { type, scope, id } = this._state.dbAction
    return \`\${scope}.\${type} (\${id})\`
  }

  public get user(): User {
    if (this._state.type === 'NotInitialized') {
      throw new Error('${execution._internalName} is not initialized yet')
    }
    return this._state.user
  }

  /**
   * Keeps track of all ActionOperations to quickly verify that any modification happens only to action operations that are in status "Started".
   */
  private readonly actionOperations = new Map<ActionOperationId, DbActionOperation>()
  private nextActionOperationId = 0

  private get id() {
    if (this._state.type === 'NotInitialized') {
      throw new Error('${execution._internalName} is not initialized yet')
    }
    return this._state.dbAction.id
  }

  constructor(
    private readonly databaseService: DatabaseService,
    private readonly config: ${actions.config.name},
    /**
     * To prevent the class from being instantiated from outside, a dummy parameter with a private type is used.
     * This way, the only way to create an instance of this class is to use the create${execution._internalName} factory function.
     */
    private readonly _doNotInitializeFromOutside: DoNotInitializeFromOutside,
  ) {}

  /**
   * Asynchronously initializes the ${execution._internalName}.
   */
  public async init({ action, user, migrationOrder }: { action: Action; user: User; migrationOrder?: number }) {
    if (this._state.type !== 'NotInitialized') {
      throw new Error('${execution._internalName} can only be initialized once')
    }

    
    try {
      const dbAction = this.config.useInMemory
        ? { scope: action.scope, type: action.type, id: (migrationOrder ?? nextActionId++).toString() }
        : await this.databaseService.action.create({
          data: {
            scope: action.scope,
            type: action.type,
            payload: JSON.stringify(action.payload),
            status: 'Started',
            user_id: user.id,
            migration_order: migrationOrder,
          },
        })
      this._state = { type: 'Started', dbAction, user }
      this.logger = new Logger(\`${execution._internalName} \${dbAction.id}\`)
    } catch (error) {
      this.logger.error(\`Error while initializing ${execution._internalName}:\`, error)
      throw error
    }
  }

  /**
   * Marks the ${execution._internalName} as finished successfully.
   */
  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
  public async success(result?: object | null | undefined | void) {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be marked as success if it is in status "Started"')
    }
    if (!this.config.useInMemory) {
      this._state.dbAction = await this.databaseService.action.update({
        where: { id: this.id },
        data: { status: 'Success', result: JSON.stringify(result) },
      })
     }

    this._state.type = 'Success'
  }

  /**
   * Marks the ${execution._internalName} as finished with an error.
   */
  public async error(error: unknown) {
    if (this._state.type !== 'Started') {
      throw new Error(
        \`${execution._internalName} can only be marked as error if it is in status "Started" - current status is: \${this._state.type}\`,
      )
    }

    const e = JSON.stringify(serializeError(error))
    
    if (!this.config.useInMemory) {
      this._state.dbAction = await this.databaseService.action.update({
        where: { id: this.id },
        data: { status: 'Error', error: e },
      })
    }
    
    this._state.type = 'Error'
    this.logger.error(\`\${this.name} finished (in \${Date.now() - this.startTimestamp}ms) with error: \${e}\`)
  }

  public async ${mutations.create.start}({
    model,
    createObject,
  }: {
    model: ModelName
    createObject: object
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'Create', payload: createObject })
  }

  public async ${mutations.createMany.start}({
    model,
    createObjects,
  }: {
    model: ModelName
    createObjects: object[]
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'CreateMany', payload: createObjects })
  }

  public async ${mutations.update.start}({
    model,
    entityId,
    sourceObject,
    updateObject,
  }: {
    model: ModelName
    entityId: string
    sourceObject: object
    updateObject: object
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'Update', entityId, sourceEntity: sourceObject, payload: updateObject })
  }

  public async ${mutations.updateMany.start}({
    model,
    sourceObjects,
    updateObjects,
  }: {
    model: ModelName
    sourceObjects: object[]
    updateObjects: object[]
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'UpdateMany', sourceEntity: sourceObjects, payload: updateObjects })
  }

  public async ${mutations.delete.start}({
    model,
    entityId,
    sourceObject,
  }: {
    model: ModelName
    entityId: string
    sourceObject: object
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'Delete', entityId, sourceEntity: sourceObject })
  }

  public async ${mutations.deleteMany.start}({
    model,
    entityIds,
    sourceObjects,
  }: {
    model: ModelName
    entityIds: string[]
    sourceObjects: object[]
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'DeleteMany', entityId: JSON.stringify(entityIds), sourceEntity: sourceObjects })
  }

  public async ${mutations.clone.start}({
    model,
    entityId,
    sourceObject,
  }: {
    model: ModelName
    entityId: string
    sourceObject: object
  }): Promise<ActionOperationId> {
    return this.startMutation({ model, kind: 'Clone', entityId, sourceEntity: sourceObject })
  }

  private async startMutation({
    model,
    kind,
    entityId,
    payload,
    sourceEntity,
  }: {
    model: ModelName
    kind: Prisma.MutationKind
    entityId?: string
    payload?: object
    sourceEntity?: object
  }): Promise<ActionOperationId> {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be started if it is in status "Started"')
    }

    const actionOperation: DbActionOperation = 
      this.config.useInMemory
      ? {
        id: toActionOperationId((this.nextActionOperationId++).toString()),
        action_id: this.id,
        status: 'Started',
        content_mutation_kind: kind,
      }
      : await this.databaseService.actionOperation.create({
        data: {
          action_id: this.id,
          order: this.nextActionOperationId++,
          
          status: 'Started',
          payload: JSON.stringify(payload),
          
          content: 'Mutation',
          
          content_mutation_model: model,
          content_mutation_kind: kind,
          content_mutation_entity_id: entityId,
          content_mutation_source_entity: JSON.stringify(sourceEntity),
        },
      })
    const actionOperationId = toActionOperationId(actionOperation.id)

    this.actionOperations.set(actionOperationId, actionOperation)

    return actionOperationId
  }

  public async ${mutations.create.finish}({
    actionOperationId,
    entityId,
    createdObject,
  }: {
    actionOperationId: ActionOperationId
    entityId: string
    createdObject: object
  }) {
    return this.finishMutation({ actionOperationId, entityId, result: createdObject })
  }

  public async ${mutations.createMany.finish}({
    actionOperationId,
    entityIds,
    createdObjects,
  }: {
    actionOperationId: ActionOperationId
    entityIds: string[]
    createdObjects: object[]
  }) {
    return this.finishMutation({ actionOperationId, entityId: JSON.stringify(entityIds), result: createdObjects })
  }

  public async ${mutations.update.finish}({ actionOperationId, updatedObject }: { actionOperationId: ActionOperationId; updatedObject: object }) {
    return this.finishMutation({ actionOperationId, result: updatedObject })
  }

  public async ${mutations.updateMany.finish}({ actionOperationId, updatedObjects }: { actionOperationId: ActionOperationId; updatedObjects: object[] }) {
    return this.finishMutation({ actionOperationId, result: updatedObjects })
  }

  public async ${mutations.delete.finish}({ actionOperationId }: { actionOperationId: ActionOperationId }) {
    return this.finishMutation({ actionOperationId })
  }

  public async ${mutations.deleteMany.finish}({ actionOperationId }: { actionOperationId: ActionOperationId }) {
    return this.finishMutation({ actionOperationId })
  }

  public async ${mutations.clone.finish}({ actionOperationId, clonedObject }: { actionOperationId: ActionOperationId; clonedObject: object }) {
    return this.finishMutation({ actionOperationId, result: clonedObject })
  }

  private async finishMutation({
    actionOperationId,
    entityId,
    result,
  }: {
    actionOperationId: ActionOperationId
    entityId?: string
    result?: object
  }) {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be finished if it is in status "Started"')
    }

    const actionOperation = this.actionOperations.get(actionOperationId)
    if (!actionOperation) {
      throw new Error(\`ActionOperation with id \${actionOperationId} not found\`)
    }

    if (actionOperation.status !== 'Started') {
      throw new Error(\`ActionOperation (Mutation) with id \${actionOperationId} is not in status "Started"\`)
    }

    if (this.config.useInMemory) {
      actionOperation.status = 'Success'
    } else {
      const updated = await this.databaseService.actionOperation.update({
        where: { id: actionOperationId },
        data: {
          status: 'Success',
          result: JSON.stringify(result),
          
          content_mutation_entity_id: entityId,
        },
      })
      this.actionOperations.set(actionOperationId, updated)
    }
  }

  async ${transformation.executeMultiple}(steps: readonly [string, (input: any) => any][], initialValue: any): Promise<any> {
    let payload = initialValue
    for (const [operation, predicate] of steps) {
      payload = await this.${transformation.executeSingle}({ operation, payload, predicate })
    }
    return payload
  }

  async ${transformation.executeSingle}<Payload extends object, Result extends object>({
    operation,
    payload,
    predicate,
  }: {
    operation: string
    payload: Payload
    predicate: (input: Payload) => Result | Promise<Result>
  }): Promise<Result> {
    const actionOperationId = await this.${transformation.start}({ operation, payload })
    try {
      const result = await predicate(payload)
      await this.${transformation.finish}({ actionOperationId, result })
      return result
    } catch (error) {
      await this.${errorOperation}({ actionOperationId, error })
      throw error
    }
  }

  public async ${transformation.start}({
    operation,
    payload,
  }: {
    operation: string
    payload: object
  }): Promise<ActionOperationId> {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be started if it is in status "Started"')
    }
    
    const actionOperation: DbActionOperation = 
      this.config.useInMemory
      ? {
        id: toActionOperationId((this.nextActionOperationId++).toString()),
        action_id: this.id,
        status: 'Started',
        content_mutation_kind: 'Transformation',
      }
      : await this.databaseService.actionOperation.create({
      data: {
        action_id: this.id,
        order: this.nextActionOperationId++,
        
        status: 'Started',
        payload: JSON.stringify(payload),
        
        content: 'Transformation',
        content_transformation_operation: operation,
      },
    })
    const actionOperationId = toActionOperationId(actionOperation.id)

    this.actionOperations.set(actionOperationId, actionOperation)

    return actionOperationId
  }

  public async ${transformation.finish}({
    actionOperationId,
    result,
  }: {
    actionOperationId: ActionOperationId
    result: object
  }) {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be finished if it is in status "Started"')
    }

    const actionOperation = this.actionOperations.get(actionOperationId)
    if (!actionOperation) {
      throw new Error(\`ActionOperation with id \${actionOperationId} not found\`)
    }

    if (actionOperation.status !== 'Started') {
      throw new Error(\`ActionOperation (Mutation) with id \${actionOperationId} is not in status "Started"\`)
    }

    if (this.config.useInMemory) {
      actionOperation.status = 'Success'
    } else {
      const updated = await this.databaseService.actionOperation.update({
        where: { id: actionOperationId },
        data: {
          status: 'Success',
          result: JSON.stringify(result),
        },
      })
  
      this.actionOperations.set(actionOperationId, updated)
    }
  }
  
  public async ${errorOperation}({ actionOperationId, error }: { actionOperationId: ActionOperationId; error: unknown }) {
    if (this._state.type !== 'Started') {
      throw new Error('${execution._internalName} can only be finished if it is in status "Started"')
    }

    const actionOperation = this.actionOperations.get(actionOperationId)
    if (!actionOperation) {
      throw new Error(\`ActionOperation with id \${actionOperationId} not found\`)
    }

    if (actionOperation.status !== 'Started') {
      throw new Error(\`ActionOperation with id \${actionOperationId} is not in status "Started"\`)
    }

    if (this.config.useInMemory) {
      actionOperation.status = 'Error'
    } else {
      const updated = await this.databaseService.actionOperation.update({
        where: { id: actionOperationId },
        data: {
          status: 'Error',
          error: JSON.stringify(serializeError(error)),
        },
      })

      this.actionOperations.set(actionOperationId, updated)
    }
  }
}
`;
}
//# sourceMappingURL=execution-class.generator.js.map