"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateRepository_InMemory = exports.generateRepository_DatabaseDirect = exports.generateRepository_DatabaseCached = void 0;
const Generator = __importStar(require("@postxl/generator"));
const schema_1 = require("@postxl/schema");
const utils_1 = require("@postxl/utils");
/**
 * Generates repository data structure for a given model that persists the data in the database and caches it in memory.
 */
const generateRepository_DatabaseCached = (model, context) => {
    const { repository, types } = model;
    const imports = new Generator.ImportGenerator({ path: repository.location })
        .addType(types)
        .addType(types.dto.create)
        .addType(types.dto.update)
        .addType(types.dto.upsert)
        .addType(types.dto.clone)
        .addType(types.id)
        .addImport({
        from: types.location,
        items: [types.id.toBranded.name],
    })
        .addType(context.actions.execution.interface)
        .addType(context.repositories.interface)
        .add(context.database.service)
        .add(model.decoder.flattened.decoder.parseFunction)
        .add(model.decoder.flattened.encoder)
        .addImport({
        from: Generator.toPackageName('@postxl/utils'),
        items: [
            Generator.toFunctionName('format'),
            Generator.toFunctionName('pluralize'),
            Generator.toFunctionName('assertArray'),
        ],
    })
        .addImport({ from: Generator.toPackageName('async-mutex'), items: [Generator.toClassName('Mutex')] });
    const params = { model, context, imports };
    const idBlocks = generateIdBlocks_Database(params);
    const defaultValueBlocks = generateDefaultBlocks(params);
    const uniqueStringFieldsBlocks = generateUniqueFieldsBlocks_Cached(params);
    const maxLengthBlocks = generateMaxLengthBlocks(params);
    const validationBlocks = generateValidationBlocks(params);
    const indexBlocks = generateIndexBlocks_Cached(params);
    const relationsBlocks = generateRelationsBlocks_Cached(params);
    const fields = Array.from(model.fields.values());
    const userRepositorySpecificBlocks = generateUserRepositorySpecificBlocks_Database(params);
    const idField = model.idField;
    /**
     * In case of Action or ActionOperation model, we do not want to log the actions, as this would lead to an endless recursion
     */
    const skipExecutionLogging = model.name == schema_1.standardModelName_Action || model.name == schema_1.standardModelName_ActionOperation;
    return `
import { Injectable, Logger } from '@nestjs/common'
${imports.generate()}

type Filter = {
  predicate: (item: ${model.types.name}) => boolean,
  // This is only relevant for the database implementation
  prisma: any
}

@Injectable()
export class ${repository.name} implements Repository<${model.types.name}, ${idField.unbrandedTypeName}> {
  protected data: Map<${model.types.id.name}, ${model.types.name}> = new Map()
  protected logger = new Logger(${repository.name}.name)

  ${relationsBlocks.mapDeclarations.join('\n')}
  
  ${defaultValueBlocks.publicVariableDeclaration}

  ${idBlocks.generateNextIdFunctionName}

  protected uniqueIds = { 
    ${uniqueStringFieldsBlocks.mapDeclarations.join(',\n')}   
  }
  
  ${indexBlocks.nestedMapDeclarations.join('\n')}

  ${userRepositorySpecificBlocks.rootUserNameConst}

  ${userRepositorySpecificBlocks.getterBlock}

  private readonly mutex = new Mutex()

  constructor(protected db: ${context.database.service.name}) {}

  public async init(${defaultValueBlocks.init.ignoreErrorVariableDeclaration}) {
    this.data.clear()

    ${relationsBlocks.clearCode.join('\n')}

    ${uniqueStringFieldsBlocks.clearCode.join('\n')}
    
    ${defaultValueBlocks.init.resetCode}

    ${indexBlocks.initCode.join('\n')}

    const data = await this.db.${repository.prismaClientName}.findMany({})

    for (const rawItem of data) {
      const item = ${model.decoder.flattened.decoder.parseFunction.name}(rawItem)
      this.set(item)

      ${defaultValueBlocks.init.setCode}
    }

    ${idBlocks.initCode}

    ${defaultValueBlocks.init.checkCode}

    ${userRepositorySpecificBlocks.initCall}

    this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.types.name}', this.data.size)} loaded\`)
    ${indexBlocks.initLogCode.join('\n')}
  }

  ${userRepositorySpecificBlocks.rootUserInitializeBlock}

  public async get(id: ${model.types.id.name} | null): Promise<${model.types.name} | null> {
    if (id === null) {
      return Promise.resolve(null) // NOSONAR - keep all validations async to adhere to the same interface
    }
    await this.mutex.waitForUnlock()
    return Promise.resolve(this.data.get(id) ?? null) // NOSONAR - keep all validations async to adhere to the same interface
  }

  public async getAll(): Promise<Map<${model.types.id.name}, ${model.types.name}>> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(new Map(this.data)) // NOSONAR - keep all validations async to adhere to the same interface
  }

  public async getAllAsArray(): Promise<${model.types.name}[]> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(Array.from(this.data.values())) // NOSONAR - keep all validations async to adhere to the same interface
  }

  ${indexBlocks.getterFunctions.join('\n')}
  
  ${uniqueStringFieldsBlocks.getByFunctions.join('\n')} 
		
  public async filter({ predicate }: Filter): Promise<${model.types.name}[]> {
    await this.mutex.waitForUnlock()
    return (await this.getAllAsArray()).filter(predicate)
  }

  public async findFirst({ predicate }: Filter): Promise<${model.types.name} | null> {
    await this.mutex.waitForUnlock()
    return (await this.getAllAsArray()).find(predicate) ?? null
  }

  public async count(): Promise<number> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(this.data.size) // NOSONAR - keep all validations async to adhere to the same interface
  }
  
  ${Generator.toJsDocComment([
        `Checks that item has the ${idField.name} field.`,
        `In case none exists, ${idBlocks.verifyFunctionComment}`,
        uniqueStringFieldsBlocks.verifyFunctionComment,
        maxLengthBlocks.verifyFunctionComment,
    ])}
  private verifyItem(
    item: ${idBlocks.verifyFunctionParameterType}
  ): ${idBlocks.createFunctionParameterType} {
    ${idBlocks.verifyCode}

    ${maxLengthBlocks.verifyCode.join('\n')}

    ${uniqueStringFieldsBlocks.verifyCode.join('\n')}

    ${validationBlocks.verifyCode.join('\n')}

  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} and returns it.`])}
  public async create(
    { item${skipIf(skipExecutionLogging, ', execution')} }: { item: ${model.types.dto.create.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    
  ${skipIf(skipExecutionLogging, `const actionOperationId = await execution.${context.actions.execution.mutations.create.start}({ 
      model: '${model.types.scopeName}', 
      createObject: item 
    })`)}
  
  const release = await this.mutex.acquire()  
  ${skipIf(!skipExecutionLogging, '// eslint-disable-next-line no-useless-catch')}
    try {    
      this.verifyItem(item)  
      const newItem = ${model.decoder.flattened.decoder.parseFunction.name}(
        await this.db.${repository.prismaClientName}.create({
          data: ${model.decoder.flattened.encoder.name}(item),
        }),
      )

      this.set(newItem)
      release()

      ${skipIf(skipExecutionLogging, `await execution.${context.actions.execution.mutations.create.finish}({ actionOperationId, createdObject: newItem, entityId: newItem.id })`)}
      return newItem
    } catch (error) {
      release()
      ${skipIf(skipExecutionLogging, `await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })`)}
      throw error
    }
  }

  ${Generator.toJsDocComment([`Creates multiple new ${model.internalPluralName} and returns them.`])}
  public async createMany(
    {items, execution}: { items: ${model.types.dto.create.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const actionOperationId = await execution.${context.actions.execution.mutations.createMany.start}({ 
      model: '${model.types.scopeName}', 
      createObjects: items 
    })
    
    const release = await this.mutex.acquire()
    try {
      assertArray(items, this.verifyItem.bind(this))
      
      const dbItems = await this.db.${repository.prismaClientName}.createManyAndReturn({ data: items.map(i => ${model.decoder.flattened.encoder.name}(i)) })
      
      const result = dbItems.map((item) => ${model.decoder.flattened.decoder.parseFunction.name}(item))

      for (const item of result) {
        this.set(item)
      }
      release()

      await execution.${context.actions.execution.mutations.createMany.finish}({
        actionOperationId,
        createdObjects: result,
        entityIds: dbItems.map((i) => i.id),
      })
      return result
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates a ${model.userFriendlyName} and returns it.`])}
  public async update(
    { item, execution }: { item: ${model.types.dto.update.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = await this.get(item.${idField.name})
    if (!existingItem) {
      throw new Error(\`Could not update ${model.userFriendlyName} with id \${item.id}. Not found!\`)
    }
    
    const actionOperationId = await execution.${context.actions.execution.mutations.update.start}({
      model: '${model.types.scopeName}',
      entityId: item.id,
      sourceObject: existingItem,
      updateObject: item,
      })
    
    const release = await this.mutex.acquire()
    try {        
      ${maxLengthBlocks.updateCode.join('\n')}
  
      ${uniqueStringFieldsBlocks.updateCode.join('\n')}
      
      const newItem = ${model.decoder.flattened.decoder.parseFunction.name}(
        await this.db.${repository.prismaClientName}.update({
          where: { 
            ${idField.databaseName}: item.${idField.name},
          },
          data: {
            ${[...fields.values()]
        .filter((f) => f.kind !== 'id' && !f.isReadonly)
        .map((f) => getFieldUpdateAssignment(f))
        .join(',\n')}       
          },
        }),
      )
  
      this.remove(existingItem)
      this.set(newItem)
      release()
  
      await execution.${context.actions.execution.mutations.update.finish}({ actionOperationId, updatedObject: newItem })
      return newItem
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates multiple ${model.internalPluralName} and returns them.`])}
  public async updateMany(
    { items, execution }: { items: ${model.types.dto.update.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.update({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Creates or updates a ${model.userFriendlyName} and returns it.`])}
  public async upsert(
    { item, execution }: { item: ${model.types.dto.upsert.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = item.${model.idField.name} ? (await this.get(item.${model.idField.name})) : null
    if (existingItem) {
      return this.update({ item: item as ${model.types.dto.update.name}, execution })
    } else {
      return this.create({ item: item as ${model.types.dto.create.name}, execution })
    }
  }

  ${Generator.toJsDocComment([`Creates or updates multiple ${model.internalPluralName} and returns them.`])}
  public async upsertMany(
    { items, execution }: { items: ${model.types.dto.upsert.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.upsert({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Deletes a ${model.userFriendlyName} and returns its id.`])}
  public async delete(
    { id, execution }: { id: ${model.types.id.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}> {
    const existingItem = await this.get(id)
    if (!existingItem) {
      throw new Error(\`Could not delete ${model.types.name} with id \${id}. Not found!\`)
    }

    const actionOperationId = await execution.${context.actions.execution.mutations.delete.start}({
      model: '${model.types.scopeName}',
      entityId: id,
      sourceObject: existingItem,
    })
    
    const release = await this.mutex.acquire()
    try {

      await this.db.${repository.prismaClientName}.delete({ where: { ${idField.databaseName}:id } })
      
      this.remove(existingItem)
      release()
      await execution.${context.actions.execution.mutations.delete.finish}({ actionOperationId })
      return id
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }

  ${Generator.toJsDocComment([`Deletes multiple ${model.internalPluralName} and returns their ids.`])}
  public async deleteMany(
    { ids, execution }: { ids: ${model.types.id.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}[]> {
    const deletedIds: ${model.types.id.name}[] = []
    for (const id of ids) {
      try {
        await this.delete({ id, execution })
        deletedIds.push(id)
      } catch {
        /* empty */
      }
    }
    return deletedIds 
  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} shallow clone and returns it.`])}
  public async clone({ id, item, execution }: {
    id: ${model.types.id.name}
    item: (item: ${model.types.name}) => ${model.types.dto.clone.name},
    execution: ${context.actions.execution.interface.name}
  }): Promise<${model.types.name}> {
    const source = await this.get(id)
    if (!source) {
      throw new Error(\`${model.userFriendlyName} with id \${id} not found\`)
    }

    const { id: _, ...data } = { ...source, ...item(source) }

    return await this.create({ item: data, execution })
  }

  ${relationsBlocks.getterFunctions.join('\n')}

  ${maxLengthBlocks.ensureMaxLengthFunctions.join('\n')}

  ${uniqueStringFieldsBlocks.ensureUniqueFunctions.join('\n\n')}

  /**
   * Function that adds/updates a given item to the internal data store, indexes, etc.
   */
  private set(item: ${model.types.name}): void {
    ${idBlocks.setCode}
    
    this.data.set(item.id, item)
    
    ${uniqueStringFieldsBlocks.setCode.join('\n')}

    ${relationsBlocks.setCode.join('\n')}

    ${indexBlocks.setCode.join('\n')}
  }

  /**
   * Function that removes a given item from the internal data store, indexes, etc.
   */
  private remove(item: ${model.types.name}): void {
    this.data.delete(item.id)
    ${uniqueStringFieldsBlocks.removeCode.join('\n')}
    
    ${relationsBlocks.removeCode.join('\n')}
      
    ${indexBlocks.removeCode.join('\n')}
  }
}

`;
};
exports.generateRepository_DatabaseCached = generateRepository_DatabaseCached;
/**
 * Generates repository data structure for a given model that persists the data in the database and does not use any in-memory caching.
 */
const generateRepository_DatabaseDirect = (model, context) => {
    const { types, repository } = model;
    const imports = new Generator.ImportGenerator({ path: repository.location })
        .addType(types)
        .addType(types.dto.create)
        .addType(types.dto.update)
        .addType(types.dto.upsert)
        .addType(types.dto.clone)
        .addType(types.id)
        .addImport({
        from: types.location,
        items: [types.id.toBranded.name],
    })
        .addType(context.actions.execution.interface)
        .addType(context.repositories.interface)
        .add(context.database.service)
        .add(model.decoder.flattened.decoder.parseFunction)
        .add(model.decoder.flattened.encoder)
        .addImport({
        from: Generator.toPackageName('@postxl/utils'),
        items: [
            Generator.toFunctionName('format'),
            Generator.toFunctionName('pluralize'),
            Generator.toFunctionName('assertArray'),
        ],
    })
        .addImport({ from: context.database.prismaClient.location, items: [Generator.toFunctionName('Prisma')] })
        .addImport({ from: Generator.toPackageName('async-mutex'), items: [Generator.toClassName('Mutex')] });
    const params = { model, context, imports };
    const idBlocks = generateIdBlocks_Database(params);
    const defaultValueBlocks = generateDefaultBlocks(params);
    const uniqueStringFieldsBlocks = generateUniqueFieldsBlocks_Database(params);
    const maxLengthBlocks = generateMaxLengthBlocks(params);
    const validationBlocks = generateValidationBlocks(params);
    const indexBlocks = generateIndexBlocks_Database(params);
    const relationsBlocks = generateRelationsBlocks_Database(params);
    const fields = Array.from(model.fields.values());
    const userRepositorySpecificBlocks = generateUserRepositorySpecificBlocks_Database(params);
    const idField = model.idField;
    const hasSetFunction = idBlocks.setCode.length > 0;
    const setFunction = hasSetFunction
        ? `
  /**
   * Function that adds/updates a given item to the internal data store, indexes, etc.
   */
  private set(item: ${model.types.name}): void {
    ${idBlocks.setCode}
  }`
        : '';
    /**
     * In case of Action or ActionOperation model, we do not want to log the actions, as this would lead to an endless recursion
     */
    const skipExecutionLogging = model.name == schema_1.standardModelName_Action || model.name == schema_1.standardModelName_ActionOperation;
    return `
import { Injectable, Logger } from '@nestjs/common'
${imports.generate()}

type Filter = {
  predicate: (item: ${model.types.name}) => boolean,
  prisma: Prisma.${model.types.name}WhereInput
}

@Injectable()
export class ${repository.name} implements Repository<${model.types.name}, ${idField.unbrandedTypeName}> {
  protected logger = new Logger(${repository.name}.name)

  ${defaultValueBlocks.publicVariableDeclaration}

  ${idBlocks.generateNextIdFunctionName}

  protected uniqueIds = { 
    ${uniqueStringFieldsBlocks.mapDeclarations.join(',\n')}   
  }
  
  ${userRepositorySpecificBlocks.rootUserNameConst}

  ${userRepositorySpecificBlocks.getterBlock}

  private readonly mutex = new Mutex()

  constructor(protected db: ${context.database.service.name}) {}  

  public async init(${defaultValueBlocks.init.ignoreErrorVariableDeclaration}) {
    
    ${uniqueStringFieldsBlocks.clearCode.join('\n')}
    
    ${defaultValueBlocks.init.resetCode}
    ${defaultValueBlocks.init.setCode}
    ${defaultValueBlocks.init.checkCode}

    ${userRepositorySpecificBlocks.initCall}

    const count = await this.db.${repository.prismaClientName}.count()

    this.logger.log(\`Database contains \${format(count)} \${pluralize('${model.types.name}', count)}, no data was cached\`)
  }

  ${userRepositorySpecificBlocks.rootUserInitializeBlock}

  public async get(id: ${model.types.id.name} | null): Promise<${model.types.name} | null> {
    if (id === null) {
      return Promise.resolve(null) // NOSONAR - keep all validations async to adhere to the same interface
    }

    await this.mutex.waitForUnlock()

    const result = await this.db.${repository.prismaClientName}.findUnique({ where: { ${idField.databaseName}: id } })
    return result ? ${model.decoder.flattened.decoder.parseFunction.name}(result) : null
  }

  public async getAll(): Promise<Map<${model.types.id.name}, ${model.types.name}>> {
    const result = new Map<${model.types.id.name}, ${model.types.name}>()
    
    await this.mutex.waitForUnlock()
    
    const data = await this.db.${repository.prismaClientName}.findMany({})
    for (const rawItem of data) {
      const item = ${model.decoder.flattened.decoder.parseFunction.name}(rawItem)
      result.set(item.id, item)
      ${hasSetFunction ? 'result.set(item.id, item)' : ''}
    }
    return result
  }

  public async getAllAsArray(): Promise<${model.types.name}[]> {
    return Array.from((await this.getAll()).values())
  }

  ${indexBlocks.getterFunctions.join('\n')}

  ${uniqueStringFieldsBlocks.getByFunctions.join('\n')} 
		
  public async filter({ prisma: where }: Filter): Promise<${model.types.name}[]> {
    await this.mutex.waitForUnlock()
    
    const data = await this.db.${repository.prismaClientName}.findMany({ where })
    return data.map((item) => ${model.decoder.flattened.decoder.parseFunction.name}(item))
  }

  public async findFirst({ prisma: where }: Filter): Promise<${model.types.name} | null> {
    await this.mutex.waitForUnlock()
    const data = await this.db.${repository.prismaClientName}.findFirst({ where })
    return data ? ${model.decoder.flattened.decoder.parseFunction.name}(data) : null
  }

  public async count(): Promise<number> {
    await this.mutex.waitForUnlock()
    return this.db.${repository.prismaClientName}.count()
  }
  
  /**
   * Ensures that item has an id.
   */
  private assertId(item: ${idBlocks.verifyFunctionParameterType}): ${idBlocks.createFunctionParameterType} {
    if (item.${idField.name} !== undefined && item.${idField.name} !== null) {
      return
    }
    ${idBlocks.verifyCode}
  }

  ${Generator.toJsDocComment([
        `Checks that item has the ${idField.name} field.`,
        `In case none exists, ${idBlocks.verifyFunctionComment}`,
        uniqueStringFieldsBlocks.verifyFunctionComment,
        maxLengthBlocks.verifyFunctionComment,
    ])}
  private async verifyItem(item: ${idBlocks.verifyFunctionParameterType}): Promise<void> {
    this.assertId(item)

    ${maxLengthBlocks.verifyCode.join('\n')}

    ${uniqueStringFieldsBlocks.verifyCode.join('\n')}

    ${validationBlocks.verifyCode.join('\n')}

  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} and returns it.`])}
  public async create(
    { item${skipIf(skipExecutionLogging, ', execution')} }: { item: ${model.types.dto.create.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    ${skipIf(skipExecutionLogging, `const actionOperationId = await execution.${context.actions.execution.mutations.create.start}({ 
      model: '${model.types.scopeName}', 
      createObject: item 
    })`)}

    const release = await this.mutex.acquire()
    ${skipIf(!skipExecutionLogging, '// eslint-disable-next-line no-useless-catch')}
    try {
      await this.verifyItem(item)
      this.assertId(item)
      const newItem = ${model.decoder.flattened.decoder.parseFunction.name}(
        await this.db.${repository.prismaClientName}.create({
          data: ${model.decoder.flattened.encoder.name}(item),
        }),
      )

      ${hasSetFunction ? 'this.set(newItem)' : ''}
      ${skipIf(skipExecutionLogging, `await execution.${context.actions.execution.mutations.create.finish}({ actionOperationId, createdObject: newItem, entityId: newItem.id })`)}
      
      release()

      return newItem
    } catch (error) {
      ${skipIf(skipExecutionLogging, `await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })`)}
      release()
      throw error
    }
  }

  ${Generator.toJsDocComment([`Creates multiple new ${model.internalPluralName} and returns them.`])}
  public async createMany(
    {items, execution}: { items: ${model.types.dto.create.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const actionOperationId = await execution.${context.actions.execution.mutations.createMany.start}({ 
      model: '${model.types.scopeName}', 
      createObjects: items 
    })
    
    const release = await this.mutex.acquire()
    try {
      for (const item of items) {
        await this.verifyItem(item)
      }
      assertArray(items, this.assertId.bind(this))
      
      const dbItems = await this.db.${repository.prismaClientName}.createManyAndReturn({ data: items.map(i => ${model.decoder.flattened.encoder.name}(i)) })
      
      const result = dbItems.map((item) => ${model.decoder.flattened.decoder.parseFunction.name}(item))


      ${hasSetFunction ? 'for (const item of result) { this.set(item) }' : ''}
      release()

      await execution.${context.actions.execution.mutations.createMany.finish}({
        actionOperationId,
        createdObjects: result,
        entityIds: dbItems.map((i) => i.id),
      })
      return result
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates a ${model.userFriendlyName} and returns it.`])}
  public async update(
    { item, execution }: { item: ${model.types.dto.update.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = await this.get(item.${idField.name})
    if (!existingItem) {
      throw new Error(\`Could not update ${model.userFriendlyName} with id \${item.id}. Not found!\`)
    }
    
    const actionOperationId = await execution.${context.actions.execution.mutations.update.start}({
      model: '${model.types.scopeName}',
      entityId: item.id,
      sourceObject: existingItem,
      updateObject: item,
    })

    const release = await this.mutex.acquire()
    try {        
      ${maxLengthBlocks.updateCode.join('\n')}
  
      ${uniqueStringFieldsBlocks.updateCode.join('\n')}
      
      const newItem = ${model.decoder.flattened.decoder.parseFunction.name}(
        await this.db.${repository.prismaClientName}.update({
          where: { 
            ${idField.databaseName}: item.${idField.name},
          },
          data: {
            ${[...fields.values()]
        .filter((f) => f.kind !== 'id' && !f.isReadonly)
        .map((f) => getFieldUpdateAssignment(f))
        .join(',\n')}       
          },
        }),
      )
  
      ${hasSetFunction ? 'this.set(newItem)' : ''}
      release()
  
      await execution.${context.actions.execution.mutations.update.finish}({ actionOperationId, updatedObject: newItem })
      return newItem
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates multiple ${model.internalPluralName} and returns them.`])}
  public async updateMany(
    { items, execution }: { items: ${model.types.dto.update.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.update({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Creates or updates a ${model.userFriendlyName} and returns it.`])}
  public async upsert(
    { item, execution }: { item: ${model.types.dto.upsert.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = item.${model.idField.name} ? (await this.get(item.${model.idField.name})) : null
    if (existingItem) {
      return this.update({ item: item as ${model.types.dto.update.name}, execution })
    } else {
      return this.create({ item: item as ${model.types.dto.create.name}, execution })
    }
  }

  ${Generator.toJsDocComment([`Creates or updates multiple ${model.internalPluralName} and returns them.`])}
  public async upsertMany(
    { items, execution }: { items: ${model.types.dto.upsert.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.upsert({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Deletes a ${model.userFriendlyName} and returns its id.`])}
  public async delete(
    { id, execution }: { id: ${model.types.id.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}> {
    const existingItem = await this.get(id)
    if (!existingItem) {
      throw new Error(\`Could not delete ${model.types.name} with id \${id}. Not found!\`)
    }

    const actionOperationId = await execution.${context.actions.execution.mutations.delete.start}({
      model: '${model.types.scopeName}',
      entityId: id,
      sourceObject: existingItem,
    })
    const release = await this.mutex.acquire()
    try {

      await this.db.${repository.prismaClientName}.delete({ where: { ${idField.databaseName}:id } })
      release()
      
      await execution.${context.actions.execution.mutations.delete.finish}({ actionOperationId })
      return id
    } catch (error) {
      release()
      await execution.${context.actions.execution.errorOperation}({ actionOperationId, error })
      throw error
    }
  }

  ${Generator.toJsDocComment([`Deletes multiple ${model.internalPluralName} and returns their ids.`])}
  public async deleteMany(
    { ids, execution }: { ids: ${model.types.id.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}[]> {
    const deletedIds: ${model.types.id.name}[] = []
    for (const id of ids) {
      try {
        await this.delete({ id, execution })
        deletedIds.push(id)
      } catch {
        /* empty */
      }
    }
    return deletedIds 
  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} shallow clone and returns it.`])}
  public async clone({ id, item, execution }: {
    id: ${model.types.id.name}
    item: (item: ${model.types.name}) => ${model.types.dto.clone.name},
    execution: ${context.actions.execution.interface.name}
  }): Promise<${model.types.name}> {
    const source = await this.get(id)
    if (!source) {
      throw new Error(\`${model.userFriendlyName} with id \${id} not found\`)
    }

    const { id: _, ...data } = { ...source, ...item(source) }

    return await this.create({ item: data, execution })
  }

  ${relationsBlocks.getterFunctions.join('\n')}

  ${maxLengthBlocks.ensureMaxLengthFunctions.join('\n')}

  ${uniqueStringFieldsBlocks.ensureUniqueFunctions.join('\n\n')}

  ${setFunction}
}

`;
};
exports.generateRepository_DatabaseDirect = generateRepository_DatabaseDirect;
/**
 * Generates repository data structure for a given model that persists the data in the database and caches it in memory.
 */
const generateRepository_InMemory = (model, context) => {
    const { types, repository } = model;
    const imports = new Generator.ImportGenerator({ path: repository.location })
        .addType(types)
        .addType(types.dto.create)
        .addType(types.dto.update)
        .addType(types.dto.upsert)
        .addType(types.dto.clone)
        .addType(types.id)
        .addImport({
        from: types.location,
        items: [types.id.toBranded.name],
    })
        .addType(context.actions.execution.interface)
        .addType(context.repositories.interface)
        .addImport({ from: Generator.toPackageName('async-mutex'), items: [Generator.toFunctionName('Mutex')] });
    const params = { model, context, imports };
    const idBlocks = generateIdBlocks_InMemory(params);
    const defaultValueBlocks = generateDefaultBlocks(params);
    const uniqueStringFieldsBlocks = generateUniqueFieldsBlocks_Cached(params);
    const maxLengthBlocks = generateMaxLengthBlocks(params);
    const validationBlocks = generateValidationBlocks(params);
    const indexBlocks = generateIndexBlocks_Cached(params);
    const relationsBlocks = generateRelationsBlocks_Cached(params);
    const userRepositorySpecificBlocks = generateUserRepositorySpecificBlocks_InMemory(params);
    const idField = model.idField;
    return `
import { Injectable, Logger } from '@nestjs/common'
${imports.generate()}

type Filter = {
  predicate: (item: ${model.types.name}) => boolean,
  // This is only relevant for the database implementation
  prisma: any
}

@Injectable()
export class ${repository.name} implements Repository<${model.types.name}, ${idField.unbrandedTypeName}> {
  protected data: Map<${model.types.id.name}, ${model.types.name}> = new Map()
  protected logger = new Logger(${repository.name}.name)

  ${relationsBlocks.mapDeclarations.join('\n')}
  
  ${defaultValueBlocks.publicVariableDeclaration}

  ${idBlocks.generateNextIdFunctionName}

  protected uniqueIds = { 
    ${uniqueStringFieldsBlocks.mapDeclarations.join(',\n')}   
  }
  
  ${indexBlocks.nestedMapDeclarations.join('\n')}

  ${userRepositorySpecificBlocks.rootUserNameConst}

  ${userRepositorySpecificBlocks.getterBlock}

  private readonly mutex = new Mutex()

  ${userRepositorySpecificBlocks.initCall === '' ? '// eslint-disable-next-line @typescript-eslint/require-await' : ''}
  public async init(${defaultValueBlocks.init.ignoreErrorVariableDeclaration}) {
    this.data.clear()

    ${relationsBlocks.clearCode.join('\n')}

    ${uniqueStringFieldsBlocks.clearCode.join('\n')}
    
    ${indexBlocks.initCode.join('\n')}

    ${idBlocks.initCode}

    ${userRepositorySpecificBlocks.initCall}

    ${indexBlocks.initLogCode.join('\n')}
  }

  ${userRepositorySpecificBlocks.rootUserInitializeBlock}

  public async get(id: ${model.types.id.name} | null): Promise<${model.types.name} | null> {
    if (id === null) {
      return Promise.resolve(null) // NOSONAR - keep all validations async to adhere to the same interface
    }
    await this.mutex.waitForUnlock()

    return Promise.resolve(this.data.get(id) ?? null) // NOSONAR - keep all validations async to adhere to the same interface
  }

  public async getAll(): Promise<Map<${model.types.id.name}, ${model.types.name}>> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(new Map(this.data)) // NOSONAR - keep all validations async to adhere to the same interface
  }

  public async getAllAsArray(): Promise<${model.types.name}[]> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(Array.from(this.data.values())) // NOSONAR - keep all validations async to adhere to the same interface
  }

  ${indexBlocks.getterFunctions.join('\n')}
  
  ${uniqueStringFieldsBlocks.getByFunctions.join('\n')} 
		
  public async filter({ predicate }: Filter): Promise<${model.types.name}[]> {
    return (await this.getAllAsArray()).filter(predicate)
  }

  public async findFirst({ predicate }: Filter): Promise<${model.types.name} | null> {
    return (await this.getAllAsArray()).find(predicate) ?? null
  }

  public async count(): Promise<number> {
    await this.mutex.waitForUnlock()
    return Promise.resolve(this.data.size) // NOSONAR - keep all validations async to adhere to the same interface
  }
  
  ${Generator.toJsDocComment([
        `Checks that item has the ${idField.name} field.`,
        `In case none exists, ${idBlocks.verifyFunctionComment}`,
        uniqueStringFieldsBlocks.verifyFunctionComment,
        maxLengthBlocks.verifyFunctionComment,
    ])}
  private verifyItem(
    item: ${idBlocks.verifyFunctionParameterType}
  ): ${idBlocks.createFunctionParameterType} {
    ${idBlocks.verifyCode}

    ${maxLengthBlocks.verifyCode.join('\n')}

    ${uniqueStringFieldsBlocks.verifyCode.join('\n')}

    ${validationBlocks.verifyCode.join('\n')}

  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} and returns it.`])}
  public async create(
    { item }: { item: ${model.types.dto.create.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const release = await this.mutex.acquire()
    try {
      this.verifyItem(item)
      this.set(item)
      release()
      return Promise.resolve(item) // NOSONAR - keep all validations async to adhere to the same interface
    } catch (error) {
      release()
      throw error
    }
  }

  ${Generator.toJsDocComment([`Creates multiple new ${model.internalPluralName} and returns them.`])}
  public async createMany(
    {items}: { items: ${model.types.dto.create.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    const release = await this.mutex.acquire()
    try {
      for (const item of items) {
        this.verifyItem(item)
        this.set(item)
        result.push(item)
      }

      release()
      return Promise.resolve(result) // NOSONAR - keep all validations async to adhere to the same interface
    } catch (error) {
      release()
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates a ${model.userFriendlyName} and returns it.`])}
  public async update(
    { item }: { item: ${model.types.dto.update.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = await this.get(item.${idField.name})
    if (!existingItem) {
      throw new Error(\`Could not update ${model.userFriendlyName} with id \${item.id}. Not found!\`)
    }
    const release = await this.mutex.acquire()
    try {
      ${maxLengthBlocks.updateCode.join('\n')}

      ${uniqueStringFieldsBlocks.updateCode.join('\n')}

      const newItem = { ...existingItem, ...item }

      this.remove(existingItem)
      this.set(newItem)

      release()
      return Promise.resolve(newItem) // NOSONAR - keep all validations async to adhere to the same interface
    } catch (error) {
      release()
      throw error
    }
  }
  
  ${Generator.toJsDocComment([`Updates multiple ${model.internalPluralName} and returns them.`])}
  public async updateMany(
    { items, execution }: { items: ${model.types.dto.update.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.update({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Creates or updates a ${model.userFriendlyName} and returns it.`])}
  public async upsert(
    { item, execution }: { item: ${model.types.dto.upsert.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}> {
    const existingItem = item.${model.idField.name} ? (await this.get(item.${model.idField.name})) : null
    if (existingItem) {
      return this.update({ item: item as ${model.types.dto.update.name}, execution })
    } else {
      return this.create({ item: item as ${model.types.dto.create.name}, execution })
    }
  }

  ${Generator.toJsDocComment([`Creates or updates multiple ${model.internalPluralName} and returns them.`])}
  public async upsertMany(
    { items, execution }: { items: ${model.types.dto.upsert.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.name}[]> {
    const result: ${model.types.name}[] = []
    for (const item of items) {
      try {
        const updated = await this.upsert({ item, execution })
        result.push(updated)
      } catch {
        /* empty */
      }
    }
    return result
  }

  ${Generator.toJsDocComment([`Deletes a ${model.userFriendlyName} and returns its id.`])}
  public async delete(
    { id }: { id: ${model.types.id.name}, execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}> {
    const existingItem = await this.get(id)
    if (!existingItem) {
      throw new Error(\`Could not delete ${model.types.name} with id \${id}. Not found!\`)
    }
    
    const release = await this.mutex.acquire()
    try {
      this.remove(existingItem)
      release()
      return id
    } catch (error) {
      release()
      throw error
    }
  }

  ${Generator.toJsDocComment([`Deletes multiple ${model.internalPluralName} and returns their ids.`])}
  public async deleteMany(
    { ids, execution }: { ids: ${model.types.id.name}[], execution: ${context.actions.execution.interface.name} }
  ): Promise<${model.types.id.name}[]> {
    const deletedIds: ${model.types.id.name}[] = []
    for (const id of ids) {
      try {
        await this.delete({ id, execution })
        deletedIds.push(id)
      } catch {
        /* empty */
      }
    }
    return deletedIds 
  }

  ${Generator.toJsDocComment([`Creates a new ${model.userFriendlyName} shallow clone and returns it.`])}
  public async clone({ id, item, execution }: {
    id: ${model.types.id.name}
    item: (item: ${model.types.name}) => ${model.types.dto.clone.name},
    execution: ${context.actions.execution.interface.name}
  }): Promise<${model.types.name}> {
    const source = await this.get(id)
    if (!source) {
      throw new Error(\`${model.userFriendlyName} with id \${id} not found\`)
    }

    const { id: _, ...data } = { ...source, ...item(source) }

    return await this.create({ item: data, execution })
  }

  ${relationsBlocks.getterFunctions.join('\n')}

  ${maxLengthBlocks.ensureMaxLengthFunctions.join('\n')}

  ${uniqueStringFieldsBlocks.ensureUniqueFunctions.join('\n\n')}

  /**
   * Function that adds/updates a given item to the internal data store, indexes, etc.
   */
  private set(item: ${model.types.name}): void {
    ${idBlocks.setCode}
    
    this.data.set(item.id, item)
    
    ${uniqueStringFieldsBlocks.setCode.join('\n')}

    ${relationsBlocks.setCode.join('\n')}

    ${indexBlocks.setCode.join('\n')}
  }

  /**
   * Function that removes a given item from the internal data store, indexes, etc.
   */
  private remove(item: ${model.types.name}): void {
    this.data.delete(item.id)
    ${uniqueStringFieldsBlocks.removeCode.join('\n')}
    
    ${relationsBlocks.removeCode.join('\n')}
      
    ${indexBlocks.removeCode.join('\n')}
  }
}

`;
};
exports.generateRepository_InMemory = generateRepository_InMemory;
const blankUserRepositorySpecificBlocks = {
    rootUserNameConst: '',
    getterBlock: '',
    initCall: '',
    rootUserInitializeBlock: '',
};
function generateUserRepositorySpecificBlocks_Database({ model, context, imports, }) {
    if (model.name !== 'User') {
        return blankUserRepositorySpecificBlocks;
    }
    const { types, repository } = model;
    const rootUserValue = generateSharedRootUserBlocks({ model, context, imports });
    return {
        rootUserNameConst: `public static readonly ROOT_USER_ID = ${types.id.toBranded.name}('${context.schema.systemUser.id}')`,
        getterBlock: `
      // We initialize the root user in the init() function
      private _rootUser!: ${types.name}
      public get rootUser(): ${types.name} {
        return this._rootUser
      }`,
        initCall: `await this.initializeRootUser()`,
        rootUserInitializeBlock: `
      private async initializeRootUser(): Promise<void> {
        const existingRootUser = await this.get(${repository.name}.ROOT_USER_ID)
        if (existingRootUser) {
          this._rootUser = existingRootUser
          this.logger.log(\`Root user \${existingRootUser.id} loaded\`)
          return
        }
        this.logger.log('Creating root user ${context.schema.systemUser.id}')
        const release = await this.mutex.acquire()
        const rawUser = await this.db.${model.repository.prismaClientName}.create({
          data: ${model.decoder.flattened.encoder.name}(${rootUserValue} as unknown as any),
        })
        const newRootUser = ${model.decoder.flattened.decoder.parseFunction.name}(rawUser)
        this.set(newRootUser)
        this._rootUser = newRootUser
        release()
      }`,
    };
}
function generateUserRepositorySpecificBlocks_InMemory({ model, context, imports, }) {
    if (model.name !== 'User') {
        return blankUserRepositorySpecificBlocks;
    }
    const { types, repository } = model;
    const rootUserValue = generateSharedRootUserBlocks({ model, context, imports });
    return {
        rootUserNameConst: `public static readonly ROOT_USER_ID = ${types.id.toBranded.name}('${context.schema.systemUser.id}')`,
        getterBlock: `
      // We initialize the root user in the init() function
      private _rootUser!: ${types.name}
      public get rootUser(): ${types.name} {
        return this._rootUser
      }`,
        initCall: `await this.initializeRootUser()`,
        rootUserInitializeBlock: `
      private async initializeRootUser(): Promise<void> {
        const existingRootUser = await this.get(${repository.name}.ROOT_USER_ID)
        if (existingRootUser) {
          this._rootUser = existingRootUser
          return
        }
        const release = await this.mutex.acquire()
        const rawUser = ${rootUserValue}

        this.verifyItem(rawUser)
        this.set(rawUser)
        this._rootUser = rawUser
        release()
      }`,
    };
}
function generateSharedRootUserBlocks({ context }) {
    const rootUser = context.schema.systemUser;
    return JSON.stringify(rootUser);
}
/**
 * Generates code chunks responsible for verifying the Id validity of a model instance and generating the id
 * value of a model with auto-generated id.
 */
function generateIdBlocks_Database({ model, imports }) {
    const { idField, types, repository } = model;
    const generatedFields = [];
    const readonlyFields = [];
    for (const field of model.fields.values()) {
        if (field.kind === 'id') {
            generatedFields.push(field);
        }
        else if (field.isReadonly) {
            readonlyFields.push(field);
            generatedFields.push(field);
        }
    }
    const verifyFunctionParameterType = `(Omit<${types.name}, ${generatedFields.map((f) => `'${f.name}'`).join(' | ')}> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`;
    const createFunctionParameterType = 
    // In case we have readonly fields, we need to omit them from the create function.
    readonlyFields.length === 0
        ? `asserts item is ${types.name}`
        : `asserts item is Omit<${types.name}, ${readonlyFields.map((f) => `'${f.name}'`).join(' |')}>`;
    switch (idField.idType) {
        case 'Increment': {
            return {
                generateNextIdFunctionName: `
          protected currentMaxId = 0
          public generateNextId(): ${types.id.name} {
            return ${types.id.toBranded.name}(++this.currentMaxId)
          }`,
                initCode: `this.currentMaxId = (await this.db.${repository.prismaClientName}.aggregate({ _max: { ${idField.databaseName}: true } }))._max.${idField.databaseName} ?? 0`,
                verifyFunctionComment: 'the id is generated by increasing the highest former id and assigned to the item.',
                verifyFunctionParameterType,
                verifyCode: `item.${idField.name} = item.${idField.name} !==undefined ? ${types.id.toBranded.name}(item.${idField.name}) : this.generateNextId()`,
                createFunctionParameterType,
                setCode: `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }`,
            };
        }
        case 'UUID': {
            imports.addImport({
                from: Generator.toPackageName('node:crypto'),
                items: [Generator.toFunctionName('randomUUID')],
            });
            return {
                generateNextIdFunctionName: `
      public generateNextId(): ${types.id.name} {
        return ${types.id.toBranded.name}(randomUUID())
      }`,
                initCode: '',
                verifyFunctionComment: 'the id is generated by using randomUUID().',
                verifyFunctionParameterType,
                verifyCode: `item.${idField.name} = item.${idField.name} ? ${types.id.toBranded.name}(item.${idField.name}) : this.generateNextId()`,
                createFunctionParameterType,
                setCode: '',
            };
        }
        default:
            throw new utils_1.ExhaustiveSwitchCheck(idField.idType);
    }
}
/**
 * Generates code chunks responsible for verifying the Id validity of a model instance and generating the id
 * value of a model with auto-generated id.
 */
function generateIdBlocks_InMemory({ model, imports }) {
    const { idField, types } = model;
    const generatedFields = [];
    let createdAtField = undefined;
    let updatedAtField = undefined;
    for (const field of model.fields.values()) {
        if (field.kind === 'id' || field.isReadonly) {
            generatedFields.push(field);
        }
        if (field.kind === 'scalar' && field.isCreatedAt) {
            createdAtField = field;
        }
        if (field.kind === 'scalar' && field.isUpdatedAt) {
            updatedAtField = field;
        }
    }
    const verifyFunctionParameterType = `(Omit<${types.name}, ${generatedFields.map((f) => `'${f.name}'`).join(' | ')}> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`;
    const verifyCodeDateFields = `
    ${createdAtField ? `;(item as any).${createdAtField.name} = new Date()` : ''}
    ${updatedAtField ? `;(item as any).${updatedAtField.name} = new Date()` : ''}`;
    const createFunctionParameterType = `asserts item is ${types.name}`;
    switch (idField.idType) {
        case 'Increment': {
            return {
                generateNextIdFunctionName: `
          protected currentMaxId = 0
          public generateNextId(): ${types.id.name} {
            return ${types.id.toBranded.name}(++this.currentMaxId)
          }`,
                initCode: `this.currentMaxId = 0`,
                verifyFunctionComment: 'the id is generated by increasing the highest former id and assigned to the item.',
                verifyFunctionParameterType,
                verifyCode: `item.${idField.name} = item.${idField.name} === undefined ? this.generateNextId() : ${types.id.toBranded.name}(item.${idField.name})` +
                    verifyCodeDateFields,
                createFunctionParameterType,
                setCode: `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }`,
            };
        }
        case 'UUID': {
            imports.addImport({
                from: Generator.toPackageName('node:crypto'),
                items: [Generator.toFunctionName('randomUUID')],
            });
            return {
                generateNextIdFunctionName: `
      public generateNextId(): ${types.id.name} {
        return ${types.id.toBranded.name}(randomUUID())
      }`,
                initCode: '',
                verifyFunctionComment: 'the id is generated by using randomUUID().',
                verifyFunctionParameterType,
                verifyCode: `item.${idField.name} = item.${idField.name} ? ${types.id.toBranded.name}(item.${idField.name}) : this.generateNextId()` +
                    verifyCodeDateFields,
                createFunctionParameterType,
                setCode: '',
            };
        }
        default:
            throw new utils_1.ExhaustiveSwitchCheck(idField.idType);
    }
}
/**
 * Returns the code chunks that define the default value property and its initialization.
 */
function generateDefaultBlocks({ model }) {
    const { types, repository } = model;
    const defaultField = model.defaultField;
    if (!defaultField) {
        return {
            init: {
                ignoreErrorVariableDeclaration: '_ignoreError: boolean',
                resetCode: '',
                setCode: '',
                checkCode: '',
            },
            publicVariableDeclaration: '',
        };
    }
    switch (repository.kind) {
        case 'DatabaseCached':
            return {
                init: {
                    ignoreErrorVariableDeclaration: 'ignoreError: boolean',
                    resetCode: `
            this.defaultValue = undefined!
          `,
                    setCode: `
            if (item.${defaultField.name}) {
              if (this.defaultValue) {
                console.warn(\`More than one default ${model.userFriendlyName} found! \${this.defaultValue.id} and \${item.id}\`)
              }
              this.defaultValue = item
            }`,
                    checkCode: `
            if (!this.defaultValue && !ignoreError) {
              throw new Error('No default ${model.userFriendlyName} found!')
            }`,
                },
                publicVariableDeclaration: `
          // We can safely skip the assignment here as this is done in the init function
          public defaultValue!: ${types.name}
        `,
            };
        case 'InMemory':
            return {
                init: {
                    ignoreErrorVariableDeclaration: '_ignoreError: boolean',
                    resetCode: `
            this.defaultValue = undefined!
          `,
                    setCode: `
            if (item.${defaultField.name}) {
              if (this.defaultValue) {
                console.warn(\`More than one default ${model.userFriendlyName} found! \${this.defaultValue.id} and \${item.id}\`)
              }
              this.defaultValue = item
            }`,
                    checkCode: `
            if (!this.defaultValue && !_ignoreError) {
              throw new Error('No default ${model.userFriendlyName} found!')
            }`,
                },
                publicVariableDeclaration: `
          // We can safely skip the assignment here as this is done in the init function
          public defaultValue!: ${types.name}
        `,
            };
        case 'DatabaseDirect':
            return {
                init: {
                    ignoreErrorVariableDeclaration: 'ignoreError: boolean',
                    resetCode: `
            this.defaultValue = undefined!
          `,
                    setCode: `
            this.defaultValue = await this.db.${repository.prismaClientName}.findUnique({ where: { ${defaultField.databaseName}: true } })`,
                    checkCode: `
            if (!this.defaultValue && !ignoreError) {
              throw new Error('No default ${model.userFriendlyName} found!')
            }`,
                },
                publicVariableDeclaration: `
          // We can safely skip the assignment here as this is done in the init function
          public defaultValue!: ${types.name}
        `,
            };
        default:
            throw new utils_1.ExhaustiveSwitchCheck(repository);
    }
}
function hasMaxLength(field) {
    return field.kind === 'scalar' && field.type === 'string' && field.validations.maxLength !== undefined;
}
function isUniqueField(field) {
    return field.isUnique;
}
function initUniqueIds() {
    return {
        mapDeclarations: [],
        getByFunctions: [],
        clearCode: [],
        verifyFunctionComment: '',
        verifyCode: [],
        updateCode: [],
        ensureUniqueFunctions: [],
        setCode: [],
        removeCode: [],
    };
}
/**
 * Generates code chunks that enforce the uniqueness of a given field.
 */
function generateUniqueFieldsBlocks_Cached({ model }) {
    const fields = [];
    for (const field of model.fields.values()) {
        if (isUniqueField(field)) {
            fields.push(field);
        }
    }
    const result = initUniqueIds();
    for (const f of fields) {
        const returnType = `string${f.isRequired ? '' : ' | null'}`;
        result.mapDeclarations.push(`'${f.name}': new Map<string, ${model.types.name}>()`);
        result.getByFunctions.push(`

      public async getBy${(0, utils_1.toPascalCase)(f.name)}(${f.name}: ${returnType}): Promise<${model.types.name} | undefined> {
        ${f.isRequired
            ? ''
            : `if (${f.name} === null) { 
          return Promise.resolve(undefined) // NOSONAR
        }`}
        await this.mutex.waitForUnlock()
        return Promise.resolve(this.uniqueIds.${f.name}.get(${f.name})) // NOSONAR
      }`);
        result.clearCode.push(`this.uniqueIds.${f.name}.clear()`);
        result.verifyCode.push(`this.${getEnsureUniqueFnName(f)}(item)`);
        result.updateCode.push(`
      if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
        this.${getEnsureUniqueFnName(f)}(item)
      }`);
        result.ensureUniqueFunctions.push(`
      /**
       * Utility function that ensures that the ${f.name} field is unique
       */      
      private ${getEnsureUniqueFnName(f)}(item: { ${f.name}?: ${returnType} }) {
          if (item.${f.name} === null || item.${f.name} === undefined) { return }
          if (!this.uniqueIds.${f.name}.has(item.${f.name})) {
            return
          }
          let counter = 1
    
          let ${f.name}: string
          const source${(0, utils_1.toPascalCase)(f.name)} =${hasMaxLength(f) ? `item.${f.name}.substring(0, ${f.validations.maxLength - 5})` : `item.${f.name}`}
    
          do {
            ${f.name} = \`\${source${(0, utils_1.toPascalCase)(f.name)}} (\${++counter})\`
          } while (this.uniqueIds.${f.name}.has(${f.name}))
    
          this.logger.log(\`${model.types.name} ${f.name} "\${item.${f.name}}" already exists. Renaming to "\${${f.name}}")\`)
          item.${f.name} = ${f.name}
        }`);
        if (f.isRequired) {
            result.setCode.push(`this.uniqueIds.${f.name}.set(item.${f.name}, item)`);
            result.removeCode.push(`this.uniqueIds.${f.name}.delete(item.${f.name})`);
        }
        else {
            result.setCode.push(`if (item.${f.name} !== null) { this.uniqueIds.${f.name}.set(item.${f.name}, item) }`);
            result.removeCode.push(`if (item.${f.name} !== null) { this.uniqueIds.${f.name}.delete(item.${f.name}) }`);
        }
    }
    if (fields.length > 1) {
        result.verifyFunctionComment = `In case a value of the fields ${fields
            .map((f) => f.name)
            .join(', ')} is not unique, it is renamed.\n`;
    }
    else if (fields.length === 1) {
        result.verifyFunctionComment = `In case the value of the field ${fields[0].name} is not unique, it is renamed.\n`;
    }
    return result;
}
/**
 * Generates code chunks that enforce the uniqueness of a given field.
 */
function generateUniqueFieldsBlocks_Database({ model, }) {
    const { types, repository } = model;
    const fields = [];
    for (const field of model.fields.values()) {
        if (isUniqueField(field)) {
            fields.push(field);
        }
    }
    const result = initUniqueIds();
    for (const f of fields) {
        const returnType = `string${f.isRequired ? '' : ' | null'}`;
        result.getByFunctions.push(`
      
      public async getBy${(0, utils_1.toPascalCase)(f.name)}(${f.name}: ${returnType}): Promise<${types.name} | undefined> {
        ${f.isRequired ? '' : `if (${f.name} === null) { return undefined }`}
        await this.mutex.waitForUnlock()
        const result = await this.db.${repository.prismaClientName}.findUnique({ where: { ${f.databaseName}: ${f.name} } })
        if (result === null) {
          return undefined
        }
        return ${model.decoder.flattened.decoder.parseFunction.name}(result)
      }`);
        result.verifyCode.push(`await this.${getEnsureUniqueFnName(f)}(item)`);
        result.updateCode.push(`
      if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
        await this.${getEnsureUniqueFnName(f)}(item)
      }`);
        result.ensureUniqueFunctions.push(`
      /**
       * Utility function that ensures that the ${f.name} field is unique
       */      
      private async ${getEnsureUniqueFnName(f)}(item: { ${f.name}: ${returnType} }) {
        if (item.${f.name} === undefined) { return }
        let counter = 1
  
        let ${f.name}: string = item.${f.name}
        const source${(0, utils_1.toPascalCase)(f.name)} =${hasMaxLength(f) ? `item.${f.name}.substring(0, ${f.validations.maxLength - 5})` : `item.${f.name}`}

        while ((await this.db.${repository.prismaClientName}.findUnique({ where: { ${f.databaseName}: ${f.name} } })) !== null) {
          ${f.name} = \`\${source${(0, utils_1.toPascalCase)(f.name)}} (\${++counter})\`
        }
        if (counter !== 1) {
          this.logger.log(\`${types.name} ${f.name} "\${item.${f.name}}" already exists. Renaming to "\${${f.name}}")\`)
          item.${f.name} = ${f.name}
        }
      }`);
    }
    if (fields.length > 1) {
        result.verifyFunctionComment = `In case a value of the fields ${fields
            .map((f) => f.name)
            .join(', ')} is not unique, it is renamed.\n`;
    }
    else if (fields.length === 1) {
        result.verifyFunctionComment = `In case the value of the field ${fields[0].name} is not unique, it is renamed.\n`;
    }
    return result;
}
function getEnsureUniqueFnName(field) {
    return `ensureUnique${(0, utils_1.toPascalCase)(field.name)}`;
}
function generateMaxLengthBlocks({ model }) {
    const fields = [];
    for (const field of model.fields.values()) {
        if (hasMaxLength(field)) {
            fields.push(field);
        }
    }
    const result = {
        verifyFunctionComment: '',
        verifyCode: [],
        updateCode: [],
        ensureMaxLengthFunctions: [],
    };
    for (const f of fields) {
        result.verifyCode.push(`this.${getEnsureMaxLengthFnName(f)}(item)`);
        result.updateCode.push(`
      if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
        this.${getEnsureMaxLengthFnName(f)}(item)
      }`);
        result.ensureMaxLengthFunctions.push(`
      /**
       * Utility function that ensures that the ${f.name} field has a max length of ${f.validations.maxLength}
       */      
      private ${getEnsureMaxLengthFnName(f)}(item: { ${f.name}?: string }) {
        if (item.${f.name} === undefined) {
          return
        }
        if (item.${f.name}.length <= ${f.validations.maxLength}) {
          return
        }
        item.${f.name} = item.${f.name}.substring(0, ${f.validations.maxLength - 4}) + \`...\`
      }`);
    }
    if (fields.length > 1) {
        result.verifyFunctionComment = `In case a value of the fields ${fields
            .map((f) => f.name)
            .join(', ')} exceeds its max length, it is truncated.\n`;
    }
    else if (fields.length === 1) {
        result.verifyFunctionComment = `In case the value of the field ${fields[0].name} exceeds its max length, it is truncated.\n`;
    }
    return result;
}
function getEnsureMaxLengthFnName(field) {
    return `ensureMaxLength${(0, utils_1.toPascalCase)(field.name)}`;
}
function generateValidationBlocks({ model }) {
    const result = {
        verifyCode: [],
    };
    for (const f of model.fields.values()) {
        if (f.kind !== 'scalar' || f.type !== 'number') {
            continue;
        }
        const itemExists = f.isRequired ? '' : `item.${f.name} !== null && `;
        if (f.validations.type === 'int') {
            result.verifyCode.push(`
      // ensure that ${f.name} is an integer
      if (${itemExists}item.${f.name} !== Math.floor(item.${f.name})) {
        throw new Error(\`Invalid value for field ${f.name}: \${item.${f.name}}. Value must be an integer.\`)
      }`);
        }
        if (f.validations.min !== undefined) {
            result.verifyCode.push(`
      // ensure that ${f.name} is at least ${f.validations.min}
      if (${itemExists}item.${f.name} < ${f.validations.min}) {
        throw new Error(\`Invalid value for field ${f.name}: \${item.${f.name}}. Value must be at least ${f.validations.min}.\`)
      }`);
        }
        if (f.validations.max !== undefined) {
            result.verifyCode.push(`
      // ensure that ${f.name} is at most ${f.validations.max}
      if (${itemExists}item.${f.name} > ${f.validations.max}) {
        throw new Error(\`Invalid value for field ${f.name}: \${item.${f.name}}. Value must be at most ${f.validations.max}.\`)
      }`);
        }
    }
    return result;
}
function initIndexBlockOptions() {
    return {
        nestedMapDeclarations: [],
        initCode: [],
        initLogCode: [],
        getterFunctions: [],
        setCode: [],
        removeCode: [],
    };
}
function generateIndexBlocks_Database({ model, imports, }) {
    const { types, repository } = model;
    const result = initIndexBlockOptions();
    const indexes = model.indexes;
    if (indexes.length === 0) {
        return result;
    }
    imports.addImport({ items: [Generator.toClassName('NestedMap')], from: Generator.toPackageName('@postxl/utils') });
    for (const index of indexes) {
        const i = getIndexDefinition({ index, model });
        result.getterFunctions.push(`
      public async getFrom${indexes.length === 1 ? 'Index' : (0, utils_1.toPascalCase)(i.name)}
      ({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
            .map((f) => `${f.name} : ${f.type}`)
            .join(';')} }): ${types.name} | null {
        return this.db.${repository.prismaClientName}.findUnique({ where: { ${i.fields.map((f) => f.name).join(',')} } }) ? ${model.decoder.flattened.decoder.parseFunction.name}(await this.db.${repository.prismaClientName}.findUnique({ where: { ${i.fields.map((f) => f.name).join(',')} } })) : null
      }`);
    }
    return result;
}
function generateIndexBlocks_Cached({ model, imports, }) {
    const result = initIndexBlockOptions();
    const indexes = model.indexes;
    if (indexes.length === 0) {
        return result;
    }
    imports.addImport({ items: [Generator.toClassName('NestedMap')], from: Generator.toPackageName('@postxl/utils') });
    for (const index of indexes) {
        const i = getIndexDefinition({ index, model });
        result.nestedMapDeclarations.push(`
      protected ${i.name} = new NestedMap<${i.fields.map((f) => f.type).join(',')}, ${model.types.name}>(
        ${i.fields.map((f) => `(x) => x.${f.name}`).join(',')}
      )`);
        result.initCode.push(`this.${i.name}.clear()`);
        result.initLogCode.push(`this.logger.log(\`\${this.${i.name}.size} ${model.types.name} loaded into index ${i.name}\`)`);
        result.getterFunctions.push(`
      public getFrom${indexes.length === 1 ? 'Index' : (0, utils_1.toPascalCase)(i.name)}
      ({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
            .map((f) => `${f.name} : ${f.type}`)
            .join(';')} }): ${model.types.name} | null {
        return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
      }`);
        result.setCode.push(`this.${i.name}.set(item)`);
        result.removeCode.push(`this.${i.name}.delete(item)`);
    }
    return result;
}
function getIndexDefinition({ index, model, }) {
    const name = (0, utils_1.toCamelCase)(`${index.map((f) => (0, utils_1.toPascalCase)(f)).join('')}Index`);
    const fields = [];
    for (const fieldName of index) {
        const field = model.fields.get(fieldName);
        if (!field) {
            throw new Error(`Index field ${fieldName} not found in model ${model.name}`);
        }
        if (field.kind !== 'relation') {
            throw new Error(`Index field ${fieldName} is not a relation in model ${model.name}`);
        }
        fields.push(field);
    }
    return { name, fields };
}
function initRelationsBlockOptions() {
    return {
        mapDeclarations: [],
        clearCode: [],
        getterFunctions: [],
        setCode: [],
        removeCode: [],
    };
}
function generateRelationsBlocks_Cached({ model, context, imports }) {
    const relations = Array.from(model.fields.values()).filter((f) => f.kind === 'relation');
    const result = initRelationsBlockOptions();
    for (const r of relations) {
        const relationModel = context.models.get(r.referencedModelName);
        imports.addType(relationModel.types.id);
        result.mapDeclarations.push(`protected ${r.name}Map: Map<${relationModel.types.id.name}, Map<${model.types.id.name}, ${model.types.name}>> = new Map()`);
        result.clearCode.push(`this.${r.name}Map.clear()`);
        result.getterFunctions.push(`
      /**
       * Function to retrieve all ${model.internalPluralName} that are related to a ${r.name}
       */
      public async getItemsFor${(0, utils_1.toPascalCase)(r.name)}(
        id: ${relationModel.types.id.name}
      ): Promise<Map<${model.types.id.name}, ${model.types.name}>> {
        const result = this.${r.name}Map.get(id)
        if (!result) {
          return Promise.resolve(new Map<${model.types.id.name}, ${model.types.name}>()) // NOSONAR - keep all validations async to adhere to the same interface
        }
        return Promise.resolve(new Map(result)) // NOSONAR - keep all validations async to adhere to the same interface
      }

      /**
       * Function to retrieve all ${model.types.id.name}s that are related to a ${r.name}
       */
      public async getIdsFor${(0, utils_1.toPascalCase)(r.name)}(
        id: ${relationModel.types.id.name}
      ): Promise<${model.types.id.name}[]> {
        const s = this.${r.name}Map.get(id)
        if (!s) {
          return Promise.resolve([]) // NOSONAR - keep all validations async to adhere to the same interface
        }

        return Promise.resolve(Array.from(s.keys())) // NOSONAR - keep all validations async to adhere to the same interface
      }`);
        result.setCode.push(`
      ${r.isRequired ? '' : `if (item.${r.name}) {`}
      let ${r.name}Map = this.${r.name}Map.get(item.${r.name})
      if (!${r.name}Map) {
        ${r.name}Map = new Map()
        this.${r.name}Map.set(item.${r.name}, ${r.name}Map)
      }
      ${r.name}Map.set(item.id, item)
      ${r.isRequired ? '' : `}`}`);
        result.removeCode.push(`
      ${r.isRequired ? '' : `if (item.${r.name}) {`}
      const ${r.name}Map = this.${r.name}Map.get(item.${r.name})
      if (${r.name}Map) {
        ${r.name}Map.delete(item.id)
        if (${r.name}Map.size === 0) {
          this.${r.name}Map.delete(item.${r.name})
        }
      }
      ${r.isRequired ? '' : '}'}
    `);
    }
    return result;
}
function generateRelationsBlocks_Database({ model, context, imports, }) {
    const { types, repository } = model;
    const relations = Array.from(model.fields.values()).filter((f) => f.kind === 'relation');
    const result = initRelationsBlockOptions();
    for (const r of relations) {
        const relationModel = context.models.get(r.referencedModelName);
        imports.addType(relationModel.types.id);
        result.getterFunctions.push(`
      /** 
       * Function to retrieve all ${model.internalPluralName} that are related to a ${r.name}
       */
      public async getItemsFor${(0, utils_1.toPascalCase)(r.name)}(
        id: ${relationModel.types.id.name}
      ): Promise<Map<${types.id.name}, ${types.name}>> {
        const data = await this.db.${repository.prismaClientName}.findMany({ where: { ${r.databaseName}: id } })
        const result =  new Map<${types.id.name}, ${types.name}>()
        for (const rawItem of data) {
          const item = ${model.decoder.flattened.decoder.parseFunction.name}(rawItem)
          result.set(item.id, item)
        }
        return result
      }

      /** 
       * Function to retrieve all ${types.id.name}s that are related to a ${r.name}
       */
      public async getIdsFor${(0, utils_1.toPascalCase)(r.name)}(
        id: ${types.id.name}
      ): Promise<${types.id.name}[]> {
        const data = await this.db.${repository.prismaClientName}.findMany({ where: { ${r.databaseName}: id } })
        return data.map(({ id }) => ${types.id.toBranded.name}(id))
      }`);
    }
    return result;
}
function getFieldUpdateAssignment(field) {
    if (field.kind !== 'discriminatedUnion') {
        return `${field.databaseName}: item.${field.name} ${field.isRequired ? `?? existingItem.${field.name}` : ''}`;
    }
    const assignments = [];
    assignments.push(`${field.name}: item.${field.name} === undefined ? existingItem.${field.name}.type : item.${field.name}.type`);
    for (const f of field.commonFields.values()) {
        assignments.push(`${f.databaseName}: item.${field.name} === undefined ? existingItem.${field.name}.${f.name} : item.${field.name}.${f.name}${f.isRequired ? `?? existingItem.${field.name}.${f.name}` : ''}`);
    }
    for (const member of field.members.values()) {
        for (const f of member.fields.values()) {
            assignments.push(`${f.databaseName}: 
          item.${field.name} === undefined 
          ? (existingItem.${field.name}.type === '${member.type}' ? existingItem.${field.name}.${f.name} : null)
          : (item.${field.name}.type === '${member.type}' ? item.${field.name}.${f.name} : null)`);
        }
    }
    return assignments.join(',\n');
}
function skipIf(condition, text) {
    return condition ? '' : text; // NOSONAR
}
//# sourceMappingURL=model-repository.generator.js.map