Combining parsing and saving makes it harder to explain where an import should stop. I developed a small independent model that first reviews decoded rows and then proposes a result.

This is not a file uploader, spreadsheet parser or production migration tool. Its contract and failure reasons identify the responsibilities a reader needs to connect for another company.

Read the input contract first

previewImport accepts decoded object rows, an id/item/quantity mapping and existing identifiers. The three fields must map to distinct column names. Incoming batches contain at most 1,000 rows. Identifiers are strings of up to forty ASCII letters, digits, underscores or hyphens; leading zeroes remain intact. Items are nonempty strings of up to eighty characters. Quantity strings from one to ninety-nine become integers.

These are explicit teaching rules rather than guesses about an arbitrary company file. Fractional quantities, composite identifiers or another item system require a deliberate extension. Inherited object properties are not treated as column values. Candidates contain only the three normalized fields, without copying unrelated columns or changing the original rows. The original material and the proposed stored representation stay separate.

Expose both useful candidates and unresolved issues

The result contains ready, candidates and issues. Every occurrence of a duplicate identifier is marked for review, rather than silently choosing the first row. An identifier already in the target also blocks application. An empty batch has an explicit empty_batch issue. Some normalizable candidates may be present while ready remains false; a useful preview is different from a complete import proposal.

An operational migration may need skipping or merging, but this example does not invent that policy. A merge extension needs its own rules for which fields may replace existing values and how document amounts and states remain consistent. The shared implementation prioritizes an explainable stopping point over automatic deduplication whose business meaning has not been agreed.

Run the review again before proposing the result

proposeImport receives the existing synthetic list, current incoming rows and mapping, and expected and current revision numbers. Different revisions return revision_conflict. Matching revisions still trigger a fresh review against existing identifiers. Any issue returns review_required. Only a successful review returns the next revision and a complete candidate list, leaving the old list and input unchanged.

This is not a database transaction. A production server must make its revision check and write atomic, and separately enforce authenticated permissions and request idempotency. The resulting teaching store is limited to 10,000 records. The module describes a small in-memory contract, not a large-scale migration engine or a locking implementation.

Explore the implementation

Complete independent synthetic implementation

// Independent synthetic staging model. No parser, database, network or client records.
const exact=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join(',')===keys;
const idOK=v=>typeof v==='string'&&/^[A-Za-z0-9_-]{1,40}$/.test(v);
const itemOK=v=>typeof v==='string'&&v.length>0&&v.length<=80;
const quantityOK=v=>Number.isSafeInteger(v)&&v>=1&&v<=99;
export function previewImport(rows,mapping,existingIds=[]) {
  if(!Array.isArray(rows)||rows.length>1000||!Array.isArray(existingIds)||existingIds.length>10000||!existingIds.every(idOK)||new Set(existingIds).size!==existingIds.length)throw new TypeError('Invalid review input');
  const mappingOK=exact(mapping,'id,item,quantity')&&Object.values(mapping).every(v=>typeof v==='string'&&/^[A-Za-z][A-Za-z0-9_]{0,31}$/.test(v))&&new Set(Object.values(mapping)).size===3;
  if(!mappingOK)return {ready:false,candidates:[],issues:[{row:null,code:'mapping'}]};
  if(!rows.length)return {ready:false,candidates:[],issues:[{row:null,code:'empty_batch'}]};
  const counts=new Map(),existing=new Set(existingIds),issues=[],candidates=[];
  for(const r of rows){const id=r&&typeof r==='object'&&Object.hasOwn(r,mapping.id)?r[mapping.id]:null;if(idOK(id))counts.set(id,(counts.get(id)||0)+1);}
  rows.forEach((r,index)=>{
    const codes=[];
    if(!r||typeof r!=='object'||Array.isArray(r)){issues.push({row:index,code:'row_shape'});return;}
    const get=key=>Object.hasOwn(r,mapping[key])?r[mapping[key]]:undefined;
    const id=get('id'),item=get('item'),quantity=get('quantity');
    if(!idOK(id))codes.push('id');
    else{if(counts.get(id)>1)codes.push('duplicate_in_batch');if(existing.has(id))codes.push('already_exists');}
    if(!itemOK(item))codes.push('item');
    if(typeof quantity!=='string'||!/^[1-9]\d?$/.test(quantity))codes.push('quantity');
    if(!codes.length)candidates.push({id,item,quantity:Number(quantity)});
    codes.forEach(code=>issues.push({row:index,code}));
  });
  return {ready:rows.length>0&&!issues.length,candidates,issues};
}
export function proposeImport(existing,rows,mapping,expectedRevision,currentRevision) {
  if(!Array.isArray(existing)||existing.length>10000||!existing.every(r=>exact(r,'id,item,quantity')&&idOK(r.id)&&itemOK(r.item)&&quantityOK(r.quantity))||new Set(existing.map(r=>r.id)).size!==existing.length)throw new TypeError('Invalid existing synthetic store');
  if(!Number.isSafeInteger(currentRevision)||currentRevision<0||currentRevision>=Number.MAX_SAFE_INTEGER||expectedRevision!==currentRevision)return {ok:false,reason:'revision_conflict'};
  const review=previewImport(rows,mapping,existing.map(r=>r.id));
  if(!review.ready)return {ok:false,reason:'review_required',issues:review.issues};
  if(existing.length+review.candidates.length>10000)return {ok:false,reason:'store_limit'};
  return {ok:true,revision:currentRevision+1,records:[...existing.map(r=>({...r})),...review.candidates]};
}
export function syntheticImport(){return {mapping:{id:'code',item:'label',quantity:'units'},rows:[{code:'001',label:'A',units:'6'},{code:'002',label:'B',units:'4'}]};}

Use separate review and apply actions

The demo provides valid input, duplicates, missing quantity, wrong mapping, an existing identifier and a changed target revision. Changing the condition invalidates the earlier review and disables application until another review passes. Successful application changes only a synthetic list in page memory. No company file is accepted and no server or persistent store is connected.

The revision-conflict condition shows a normal candidate being rejected later because the target changed. A ready indicator is therefore not presented as a completed save. Both language demos inline the same implementation and require no external library or network request. A real interface should connect machine-readable issue codes to an understandable sentence and the place where the user can correct a row.

Independent synthetic example; no client records or operational server

Share the package and its verified scope

The ZIP contains the implementation, independent tests, standalone Korean and English HTML demos, boundaries and reuse permission. Extract it and run node test.mjs before opening the demos or adapting the code. Modification, redistribution and commercial reuse are permitted without warranty. The files were newly written for this explanation, not extracted from client operational software.

I checked 24 test groups in the extracted package and all ninety-nine accepted quantities. Tests include unchanged inputs, every duplicate occurrence, edited input after review, invalid existing records and revision boundaries. Demo events were evaluated with code adapters, not a rendered browser or operational file. CSV or spreadsheet parsing, encoding, company codes, actual storage and migration reconciliation remain separate integration work.

Download code, tests and bilingual demos
Includes reuse permission and boundaries