@stacksjs/commerce 0.75.318 → 0.75.320

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{insertedId}from"../utils/inserted-id";import{fetchById}from"./fetch";export const DEFAULT_CUSTOMER_AVATAR="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&fit=crop&q=60";export async function store(data){try{const customerData={...data,avatar:data.avatar??DEFAULT_CUSTOMER_AVATAR},result=await db.insertInto("customers").values(customerData).executeTakeFirst();if(!result)throw Error("Failed to create customer");const id=insertedId(result);if(id===void 0)throw Error("Failed to create customer: the database reported no insert id");return await fetchById(id)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A customer with this email already exists");if(error instanceof Error)throw Error(`Failed to create customer: ${error.message}`);throw error}}
1
+ import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{insertedId}from"../utils/inserted-id";import{fetchById}from"./fetch";export const DEFAULT_CUSTOMER_AVATAR="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&fit=crop&q=60";export async function store(data){try{const customerData={...data,avatar:data.avatar??DEFAULT_CUSTOMER_AVATAR},result=await db.insertInto("customers").values(customerData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create customer");const id=insertedId(result);if(id===void 0)throw Error("Failed to create customer: the database reported no insert id");return await fetchById(id)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A customer with this email already exists");if(error instanceof Error)throw Error(`Failed to create customer: ${error.message}`);throw error}}
@@ -1,4 +1,10 @@
1
1
  import type { GiftCard, ModelRow, NewModelData } from '@stacksjs/orm';
2
+ /**
3
+ * The card's dates in the form their datetime columns store. The timestamp
4
+ * rule accepts Unix seconds, and the dashboard sends them, which PostgreSQL
5
+ * refuses in a datetime column (stacksjs/stacks#3006).
6
+ */
7
+ export declare function giftCardDates(data: Record<string, unknown>): Record<string, unknown>;
2
8
  /**
3
9
  * Create a new gift card
4
10
  *
@@ -1 +1 @@
1
- import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{insertedId}from"../utils/inserted-id";export async function store(data){const d=data,giftCardData={...data,current_balance:d.current_balance??d.initial_balance,status:data.status||"ACTIVE",is_active:d.is_active??!0,uuid:randomUUIDv7()};try{const createdGiftCard=await db.insertInto("gift_cards").values(giftCardData).executeTakeFirst(),id=insertedId(createdGiftCard);if(id!==void 0){const giftCard=await db.selectFrom("gift_cards").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(giftCard)}const giftCard=await db.selectFrom("gift_cards").where("uuid","=",giftCardData.uuid).selectAll().executeTakeFirst();return asModelRow(giftCard,!0)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A gift card with this code already exists");if(error instanceof Error)throw Error(`Failed to create gift card: ${error.message}`);throw error}}
1
+ import{randomUUIDv7}from"bun";import{db,sqlDateTimeInput}from"@stacksjs/database/runtime";import{asModelRow}from"../utils/model-row";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{insertedId}from"../utils/inserted-id";export function giftCardDates(data){const dates={};for(const column of["expiry_date","last_used_date"])if(Object.prototype.hasOwnProperty.call(data,column))dates[column]=sqlDateTimeInput(data[column]);return dates}export async function store(data){const d=data,giftCardData={...data,...giftCardDates(d),current_balance:d.current_balance??d.initial_balance,status:data.status||"ACTIVE",is_active:d.is_active??!0,uuid:randomUUIDv7()};try{const createdGiftCard=await db.insertInto("gift_cards").values(giftCardData).executeTakeFirst(),id=insertedId(createdGiftCard);if(id!==void 0){const giftCard=await db.selectFrom("gift_cards").where("id","=",id).selectAll().executeTakeFirst();return asModelRow(giftCard)}const giftCard=await db.selectFrom("gift_cards").where("uuid","=",giftCardData.uuid).selectAll().executeTakeFirst();return asModelRow(giftCard,!0)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A gift card with this code already exists");if(error instanceof Error)throw Error(`Failed to create gift card: ${error.message}`);throw error}}
@@ -1,4 +1,4 @@
1
- import{db,mutationCount,parseSqlDateTime,sqlHelpers}from"@stacksjs/database/runtime";import{env}from"@stacksjs/env";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{fetchById}from"./fetch";export async function update(id,data){if(!await fetchById(id))return;try{await db.updateTable("gift_cards").set({...data,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A gift card with this code already exists");if(error instanceof Error)throw Error(`Failed to update gift card: ${error.message}`);throw error}}export async function updateBalance(id,amount){if(!Number.isFinite(amount))throw Error(`Gift card balance adjustment must be a finite number, got ${amount}`);const dialect=sqlHelpers(env.DB_CONNECTION||"sqlite"),p=dialect.param,now=formatDate(new Date),isRedemption=amount<0,isReload=amount>0,statusPredicate=isReload?`(status = 'ACTIVE' OR (status = 'USED' AND is_reloadable = ${dialect.boolTrue}))`:"status = 'ACTIVE'",values=[],bind=(value)=>{values.push(value);return p(values.length)},statement=await db.unsafe(`UPDATE gift_cards
1
+ import{db,mutationCount,parseSqlDateTime,sqlHelpers}from"@stacksjs/database/runtime";import{env}from"@stacksjs/env";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{fetchById}from"./fetch";import{giftCardDates}from"./store";export async function update(id,data){if(!await fetchById(id))return;try{await db.updateTable("gift_cards").set({...data,...giftCardDates(data),updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A gift card with this code already exists");if(error instanceof Error)throw Error(`Failed to update gift card: ${error.message}`);throw error}}export async function updateBalance(id,amount){if(!Number.isFinite(amount))throw Error(`Gift card balance adjustment must be a finite number, got ${amount}`);const dialect=sqlHelpers(env.DB_CONNECTION||"sqlite"),p=dialect.param,now=formatDate(new Date),isRedemption=amount<0,isReload=amount>0,statusPredicate=isReload?`(status = 'ACTIVE' OR (status = 'USED' AND is_reloadable = ${dialect.boolTrue}))`:"status = 'ACTIVE'",values=[],bind=(value)=>{values.push(value);return p(values.length)},statement=await db.unsafe(`UPDATE gift_cards
2
2
  SET status = CASE WHEN current_balance + ${bind(amount)} = 0 THEN 'USED' WHEN 1 = 0 THEN status ELSE 'ACTIVE' END,
3
3
  current_balance = current_balance + ${bind(amount)},
4
4
  ${isRedemption?`last_used_date = ${bind(now)},
@@ -91,6 +91,7 @@ export declare const SKU_MAX: 100;
91
91
  export declare interface ProductRow {
92
92
  uuid: string
93
93
  name: string
94
+ slug?: string
94
95
  description: string | null
95
96
  price: number
96
97
  image_url: string | null
@@ -1 +1 @@
1
- import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{normalizeStoreUrl,storeHost}from"./http";import{catalogUuid}from"./identity";export const UNKNOWN_COUNTRY="Unknown";const DEFAULT_PREPARATION_TIME=1,TYPE_MAX=50;export const SKU_MAX=100;export function lowestPrice(variants){const prices=variants.map((variant)=>variant.priceMinor).filter((price)=>price!==null);return prices.length>0?Math.min(...prices):null}export function totalInventory(variants){if(variants.length===0||variants.some((variant)=>variant.inventory===null))return null;return variants.reduce((sum,variant)=>sum+(variant.inventory??0),0)}export function variantSkus(product){const skus=new Map,warnings=[],claimed=new Set;for(const variant of product.variants){const sku=variant.sku;if(sku===null)skus.set(variant.externalId,null);else if(sku.length>SKU_MAX){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" has a SKU longer than ${SKU_MAX} characters; imported without it`})}else if(claimed.has(sku)){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" repeats SKU ${sku}; imported without it`})}else{claimed.add(sku);skus.set(variant.externalId,sku)}}return{skus,warnings}}export function productRow(product,host,refs,now,mode){const inventory=totalInventory(product.variants),row={uuid:catalogUuid(product.source,host,"product",product.externalId),name:product.name,description:product.descriptionHtml,price:lowestPrice(product.variants)??0,image_url:product.images[0]?.src??null,is_available:product.variants.some((variant)=>variant.available),updated_at:now};if(mode==="create"){row.inventory_count=inventory??0;row.preparation_time=DEFAULT_PREPARATION_TIME;row.category_id=refs.categoryId;row.manufacturer_id=refs.manufacturerId;row.created_at=now}else{if(inventory!==null)row.inventory_count=inventory;if(refs.categoryId!==null)row.category_id=refs.categoryId;if(refs.manufacturerId!==null)row.manufacturer_id=refs.manufacturerId}return row}export function variantRow(product,variant,host,productId,now,mode,sku=variant.sku){const row={uuid:catalogUuid(product.source,host,"variant",variant.externalId),product_id:productId,variant:variant.title,type:(product.optionNames.join(" / ")||"Option").slice(0,TYPE_MAX),options:JSON.stringify(variant.optionValues),status:variant.available?"active":"inactive",price:variant.priceMinor,compare_at_price:variant.compareAtMinor,updated_at:now};if(mode==="create"){row.sku=sku;row.inventory_count=variant.inventory;row.created_at=now}else{if(sku!==null)row.sku=sku;if(variant.inventory!==null)row.inventory_count=variant.inventory}return row}function withoutUuid(row){const changes={...row};delete changes.uuid;return changes}export async function importCatalog(options){const storeUrl=normalizeStoreUrl(options.storeUrl),host=storeHost(storeUrl),clock=options.now??(()=>new Date),{adapter,repository}=options,result={source:adapter.name,storeUrl,dryRun:options.dryRun===!0,pages:[],products:[],counts:{products:{created:0,updated:0},variants:{created:0,updated:0},categories:{created:0,updated:0},manufacturers:{created:0,updated:0},duplicates:0},currencies:[],warnings:[]},categoryIds=new Map,manufacturerIds=new Map,seenProducts=new Map,currencies=new Set;async function categoryFor(product,repo,now){const category=product.categories[0];if(!category)return null;const cached=categoryIds.get(category.slug);if(cached!==void 0)return cached;let id=await repo.findCategory(category.slug,category.name);if(id===void 0){id=await repo.createCategory({uuid:randomUUIDv7(),name:category.name,slug:category.slug,description:null,image_url:null,is_active:!0,parent_category_id:null,display_order:categoryIds.size,created_at:now,updated_at:now});result.counts.categories.created++}categoryIds.set(category.slug,id);return id}async function manufacturerFor(product,repo,now){if(!product.vendor)return null;const cached=manufacturerIds.get(product.vendor);if(cached!==void 0)return cached;let id=await repo.findManufacturer(product.vendor);if(id===void 0){id=await repo.createManufacturer({uuid:randomUUIDv7(),manufacturer:product.vendor,description:null,country:UNKNOWN_COUNTRY,featured:!1,created_at:now,updated_at:now});result.counts.manufacturers.created++}manufacturerIds.set(product.vendor,id);return id}async function existingFor(products){const productUuids=products.map((product)=>catalogUuid(product.source,host,"product",product.externalId)),variantUuids=products.filter((product)=>product.hasOptions).flatMap((product)=>product.variants.map((variant)=>catalogUuid(product.source,host,"variant",variant.externalId)));return{products:productUuids.length>0?await repository.findProducts(productUuids):new Map,variants:variantUuids.length>0?await repository.findVariants(variantUuids):new Map}}async function write(product,repo,known){const now=formatDate(clock()),refs={categoryId:await categoryFor(product,repo,now),manufacturerId:await manufacturerFor(product,repo,now)},uuid=catalogUuid(product.source,host,"product",product.externalId),existing=known.products.get(uuid),action=existing===void 0?"create":"update";let productId;if(existing===void 0)productId=await repo.createProduct(productRow(product,host,refs,now,"create"));else{productId=existing;await repo.updateProduct(productId,withoutUuid(productRow(product,host,refs,now,"update")))}const variants={create:0,update:0};if(product.hasOptions){const{skus,warnings}=variantSkus(product);result.warnings.push(...warnings);const claimed=[...skus.values()].filter((sku)=>sku!==null);if(existing!==void 0&&claimed.length>0)await repo.releaseVariantSkus(productId,claimed);for(const variant of product.variants){const sku=skus.get(variant.externalId)??null,found=known.variants.get(catalogUuid(product.source,host,"variant",variant.externalId));if(found===void 0){await repo.createVariant(variantRow(product,variant,host,productId,now,"create",sku));variants.create++}else{await repo.updateVariant(found,withoutUuid(variantRow(product,variant,host,productId,now,"update",sku)));variants.update++}}}return{action,externalId:product.externalId,name:product.name,handle:product.handle,priceMinor:lowestPrice(product.variants),currency:product.currency,variants,category:product.categories[0]?.name??null,vendor:product.vendor,imageCount:product.images.length}}for await(const page of adapter.pages(storeUrl,{limit:options.limit,fetch:options.fetch,currency:options.currency,accessKey:options.accessKey})){result.pages.push(page.url);result.warnings.push(...page.warnings);const fresh=[];for(const product of page.products){if(seenProducts.get(product.externalId)!==void 0){result.counts.duplicates++;result.warnings.push({externalId:product.externalId,message:`"${product.name}" appeared twice; imported once`});continue}seenProducts.set(product.externalId,product.handle);fresh.push(product)}const known=await existingFor(fresh);for(const product of fresh){if(product.currency)currencies.add(product.currency);const imported=repository.transaction?await repository.transaction((repo)=>write(product,repo,known)):await write(product,repository,known);result.counts.products[imported.action==="create"?"created":"updated"]++;result.counts.variants.created+=imported.variants.create;result.counts.variants.updated+=imported.variants.update;result.products.push(imported);options.onProduct?.(imported)}}result.currencies=[...currencies].sort();if(result.currencies.length>1)result.warnings.push({message:`prices arrived in more than one currency (${result.currencies.join(", ")}); products.price has no currency column`});return result}export function createDatabaseRepository(connection=db){async function idByUuid(table,uuid){const row=await connection.selectFrom(table).where("uuid","=",uuid).select("id").executeTakeFirst();if(!row)throw Error(`Inserted a row into ${table} but could not read it back (uuid ${uuid})`);return Number(row.id)}async function idsByUuid(table,uuids){const found=new Map;for(let start=0;start<uuids.length;start+=500){const rows=await connection.selectFrom(table).where("uuid","in",uuids.slice(start,start+500)).select(["id","uuid"]).execute();for(const row of rows)found.set(row.uuid,Number(row.id))}return found}async function first(query){const row=await query.select("id").executeTakeFirst();return row?Number(row.id):void 0}const repository={async findCategory(slug,name){return await first(connection.selectFrom("categories").where("slug","=",slug))??await first(connection.selectFrom("categories").where("name","=",name))},async createCategory(row){await connection.insertInto("categories").values(row).execute();return idByUuid("categories",row.uuid)},async findManufacturer(name){return first(connection.selectFrom("manufacturers").where("manufacturer","=",name))},async createManufacturer(row){await connection.insertInto("manufacturers").values(row).execute();return idByUuid("manufacturers",row.uuid)},async findProducts(uuids){return idsByUuid("products",uuids)},async createProduct(row){await connection.insertInto("products").values(row).execute();return idByUuid("products",row.uuid)},async updateProduct(id,row){await connection.updateTable("products").set(row).where("id","=",id).execute()},async findVariants(uuids){return idsByUuid("product_variants",uuids)},async createVariant(row){await connection.insertInto("product_variants").values(row).execute();return idByUuid("product_variants",row.uuid)},async updateVariant(id,row){await connection.updateTable("product_variants").set(row).where("id","=",id).execute()},async releaseVariantSkus(productId,skus){for(let start=0;start<skus.length;start+=500)await connection.updateTable("product_variants").set({sku:null}).where("product_id","=",productId).where("sku","in",skus.slice(start,start+500)).execute()}};if(connection===db)repository.transaction=(work)=>db.transaction((trx)=>work(createDatabaseRepository(trx)));return repository}export function createMemoryRepository(){const tables={categories:[],manufacturers:[],products:[],product_variants:[]};function insert(table,row){const id=tables[table].length+1;tables[table].push({id,...row});return id}function update(table,id,row){const target=tables[table].find((entry)=>entry.id===id);if(target)Object.assign(target,row)}const byUuid=(table,uuids)=>new Map(tables[table].filter((row)=>uuids.includes(row.uuid)).map((row)=>[row.uuid,row.id])),find=(table,match)=>tables[table].find(match)?.id;return{tables,findCategory:async(slug,name)=>find("categories",(row)=>row.slug===slug)??find("categories",(row)=>row.name===name),createCategory:async(row)=>insert("categories",row),findManufacturer:async(name)=>find("manufacturers",(row)=>row.manufacturer===name),createManufacturer:async(row)=>insert("manufacturers",row),findProducts:async(uuids)=>byUuid("products",uuids),createProduct:async(row)=>insert("products",row),updateProduct:async(id,row)=>update("products",id,row),findVariants:async(uuids)=>byUuid("product_variants",uuids),createVariant:async(row)=>insert("product_variants",row),updateVariant:async(id,row)=>update("product_variants",id,row),releaseVariantSkus:async(productId,skus)=>{for(const row of tables.product_variants)if(row.product_id===productId&&skus.includes(row.sku))row.sku=null}}}export function createReadOnlyRepository(inner){let nextId=-1;const fake=async()=>nextId--;return{findCategory:inner.findCategory,findManufacturer:inner.findManufacturer,findProducts:inner.findProducts,findVariants:inner.findVariants,createCategory:fake,createManufacturer:fake,createProduct:fake,createVariant:fake,updateProduct:async()=>{},updateVariant:async()=>{},releaseVariantSkus:async()=>{}}}
1
+ import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{uniqueSlug}from"@stacksjs/slug";import{normalizeStoreUrl,storeHost}from"./http";import{catalogUuid}from"./identity";export const UNKNOWN_COUNTRY="Unknown";const DEFAULT_PREPARATION_TIME=1,TYPE_MAX=50;export const SKU_MAX=100;export function lowestPrice(variants){const prices=variants.map((variant)=>variant.priceMinor).filter((price)=>price!==null);return prices.length>0?Math.min(...prices):null}export function totalInventory(variants){if(variants.length===0||variants.some((variant)=>variant.inventory===null))return null;return variants.reduce((sum,variant)=>sum+(variant.inventory??0),0)}export function variantSkus(product){const skus=new Map,warnings=[],claimed=new Set;for(const variant of product.variants){const sku=variant.sku;if(sku===null)skus.set(variant.externalId,null);else if(sku.length>SKU_MAX){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" has a SKU longer than ${SKU_MAX} characters; imported without it`})}else if(claimed.has(sku)){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" repeats SKU ${sku}; imported without it`})}else{claimed.add(sku);skus.set(variant.externalId,sku)}}return{skus,warnings}}export function productRow(product,host,refs,now,mode){const inventory=totalInventory(product.variants),row={uuid:catalogUuid(product.source,host,"product",product.externalId),name:product.name,description:product.descriptionHtml,price:lowestPrice(product.variants)??0,image_url:product.images[0]?.src??null,is_available:product.variants.some((variant)=>variant.available),updated_at:now};if(mode==="create"){row.slug=product.handle||product.name;row.inventory_count=inventory??0;row.preparation_time=DEFAULT_PREPARATION_TIME;row.category_id=refs.categoryId;row.manufacturer_id=refs.manufacturerId;row.created_at=now}else{if(inventory!==null)row.inventory_count=inventory;if(refs.categoryId!==null)row.category_id=refs.categoryId;if(refs.manufacturerId!==null)row.manufacturer_id=refs.manufacturerId}return row}export function variantRow(product,variant,host,productId,now,mode,sku=variant.sku){const row={uuid:catalogUuid(product.source,host,"variant",variant.externalId),product_id:productId,variant:variant.title,type:(product.optionNames.join(" / ")||"Option").slice(0,TYPE_MAX),options:JSON.stringify(variant.optionValues),status:variant.available?"active":"inactive",price:variant.priceMinor,compare_at_price:variant.compareAtMinor,updated_at:now};if(mode==="create"){row.sku=sku;row.inventory_count=variant.inventory;row.created_at=now}else{if(sku!==null)row.sku=sku;if(variant.inventory!==null)row.inventory_count=variant.inventory}return row}function withoutUuid(row){const changes={...row};delete changes.uuid;return changes}export async function importCatalog(options){const storeUrl=normalizeStoreUrl(options.storeUrl),host=storeHost(storeUrl),clock=options.now??(()=>new Date),{adapter,repository}=options,result={source:adapter.name,storeUrl,dryRun:options.dryRun===!0,pages:[],products:[],counts:{products:{created:0,updated:0},variants:{created:0,updated:0},categories:{created:0,updated:0},manufacturers:{created:0,updated:0},duplicates:0},currencies:[],warnings:[]},categoryIds=new Map,manufacturerIds=new Map,seenProducts=new Map,currencies=new Set;async function categoryFor(product,repo,now){const category=product.categories[0];if(!category)return null;const cached=categoryIds.get(category.slug);if(cached!==void 0)return cached;let id=await repo.findCategory(category.slug,category.name);if(id===void 0){id=await repo.createCategory({uuid:randomUUIDv7(),name:category.name,slug:category.slug,description:null,image_url:null,is_active:!0,parent_category_id:null,display_order:categoryIds.size,created_at:now,updated_at:now});result.counts.categories.created++}categoryIds.set(category.slug,id);return id}async function manufacturerFor(product,repo,now){if(!product.vendor)return null;const cached=manufacturerIds.get(product.vendor);if(cached!==void 0)return cached;let id=await repo.findManufacturer(product.vendor);if(id===void 0){id=await repo.createManufacturer({uuid:randomUUIDv7(),manufacturer:product.vendor,description:null,country:UNKNOWN_COUNTRY,featured:!1,created_at:now,updated_at:now});result.counts.manufacturers.created++}manufacturerIds.set(product.vendor,id);return id}async function existingFor(products){const productUuids=products.map((product)=>catalogUuid(product.source,host,"product",product.externalId)),variantUuids=products.filter((product)=>product.hasOptions).flatMap((product)=>product.variants.map((variant)=>catalogUuid(product.source,host,"variant",variant.externalId)));return{products:productUuids.length>0?await repository.findProducts(productUuids):new Map,variants:variantUuids.length>0?await repository.findVariants(variantUuids):new Map}}async function write(product,repo,known){const now=formatDate(clock()),refs={categoryId:await categoryFor(product,repo,now),manufacturerId:await manufacturerFor(product,repo,now)},uuid=catalogUuid(product.source,host,"product",product.externalId),existing=known.products.get(uuid),action=existing===void 0?"create":"update";let productId;if(existing===void 0)productId=await repo.createProduct(productRow(product,host,refs,now,"create"));else{productId=existing;await repo.updateProduct(productId,withoutUuid(productRow(product,host,refs,now,"update")))}const variants={create:0,update:0};if(product.hasOptions){const{skus,warnings}=variantSkus(product);result.warnings.push(...warnings);const claimed=[...skus.values()].filter((sku)=>sku!==null);if(existing!==void 0&&claimed.length>0)await repo.releaseVariantSkus(productId,claimed);for(const variant of product.variants){const sku=skus.get(variant.externalId)??null,found=known.variants.get(catalogUuid(product.source,host,"variant",variant.externalId));if(found===void 0){await repo.createVariant(variantRow(product,variant,host,productId,now,"create",sku));variants.create++}else{await repo.updateVariant(found,withoutUuid(variantRow(product,variant,host,productId,now,"update",sku)));variants.update++}}}return{action,externalId:product.externalId,name:product.name,handle:product.handle,priceMinor:lowestPrice(product.variants),currency:product.currency,variants,category:product.categories[0]?.name??null,vendor:product.vendor,imageCount:product.images.length}}for await(const page of adapter.pages(storeUrl,{limit:options.limit,fetch:options.fetch,currency:options.currency,accessKey:options.accessKey})){result.pages.push(page.url);result.warnings.push(...page.warnings);const fresh=[];for(const product of page.products){if(seenProducts.get(product.externalId)!==void 0){result.counts.duplicates++;result.warnings.push({externalId:product.externalId,message:`"${product.name}" appeared twice; imported once`});continue}seenProducts.set(product.externalId,product.handle);fresh.push(product)}const known=await existingFor(fresh);for(const product of fresh){if(product.currency)currencies.add(product.currency);const imported=repository.transaction?await repository.transaction((repo)=>write(product,repo,known)):await write(product,repository,known);result.counts.products[imported.action==="create"?"created":"updated"]++;result.counts.variants.created+=imported.variants.create;result.counts.variants.updated+=imported.variants.update;result.products.push(imported);options.onProduct?.(imported)}}result.currencies=[...currencies].sort();if(result.currencies.length>1)result.warnings.push({message:`prices arrived in more than one currency (${result.currencies.join(", ")}); products.price has no currency column`});return result}export function createDatabaseRepository(connection=db){async function idByUuid(table,uuid){const row=await connection.selectFrom(table).where("uuid","=",uuid).select("id").executeTakeFirst();if(!row)throw Error(`Inserted a row into ${table} but could not read it back (uuid ${uuid})`);return Number(row.id)}async function idsByUuid(table,uuids){const found=new Map;for(let start=0;start<uuids.length;start+=500){const rows=await connection.selectFrom(table).where("uuid","in",uuids.slice(start,start+500)).select(["id","uuid"]).execute();for(const row of rows)found.set(row.uuid,Number(row.id))}return found}async function first(query){const row=await query.select("id").executeTakeFirst();return row?Number(row.id):void 0}const repository={async findCategory(slug,name){return await first(connection.selectFrom("categories").where("slug","=",slug))??await first(connection.selectFrom("categories").where("name","=",name))},async createCategory(row){await connection.insertInto("categories").values(row).execute();return idByUuid("categories",row.uuid)},async findManufacturer(name){return first(connection.selectFrom("manufacturers").where("manufacturer","=",name))},async createManufacturer(row){await connection.insertInto("manufacturers").values(row).execute();return idByUuid("manufacturers",row.uuid)},async findProducts(uuids){return idsByUuid("products",uuids)},async createProduct(row){const slug=await uniqueSlug(row.slug||row.name,{table:"products",column:"slug",connection});await connection.insertInto("products").values({...row,slug}).execute();return idByUuid("products",row.uuid)},async updateProduct(id,row){await connection.updateTable("products").set(row).where("id","=",id).execute()},async findVariants(uuids){return idsByUuid("product_variants",uuids)},async createVariant(row){await connection.insertInto("product_variants").values(row).execute();return idByUuid("product_variants",row.uuid)},async updateVariant(id,row){await connection.updateTable("product_variants").set(row).where("id","=",id).execute()},async releaseVariantSkus(productId,skus){for(let start=0;start<skus.length;start+=500)await connection.updateTable("product_variants").set({sku:null}).where("product_id","=",productId).where("sku","in",skus.slice(start,start+500)).execute()}};if(connection===db)repository.transaction=(work)=>db.transaction((trx)=>work(createDatabaseRepository(trx)));return repository}export function createMemoryRepository(){const tables={categories:[],manufacturers:[],products:[],product_variants:[]};function insert(table,row){const id=tables[table].length+1;tables[table].push({id,...row});return id}function update(table,id,row){const target=tables[table].find((entry)=>entry.id===id);if(target)Object.assign(target,row)}const byUuid=(table,uuids)=>new Map(tables[table].filter((row)=>uuids.includes(row.uuid)).map((row)=>[row.uuid,row.id])),find=(table,match)=>tables[table].find(match)?.id;return{tables,findCategory:async(slug,name)=>find("categories",(row)=>row.slug===slug)??find("categories",(row)=>row.name===name),createCategory:async(row)=>insert("categories",row),findManufacturer:async(name)=>find("manufacturers",(row)=>row.manufacturer===name),createManufacturer:async(row)=>insert("manufacturers",row),findProducts:async(uuids)=>byUuid("products",uuids),createProduct:async(row)=>insert("products",row),updateProduct:async(id,row)=>update("products",id,row),findVariants:async(uuids)=>byUuid("product_variants",uuids),createVariant:async(row)=>insert("product_variants",row),updateVariant:async(id,row)=>update("product_variants",id,row),releaseVariantSkus:async(productId,skus)=>{for(const row of tables.product_variants)if(row.product_id===productId&&skus.includes(row.sku))row.sku=null}}}export function createReadOnlyRepository(inner){let nextId=-1;const fake=async()=>nextId--;return{findCategory:inner.findCategory,findManufacturer:inner.findManufacturer,findProducts:inner.findProducts,findVariants:inner.findVariants,createCategory:fake,createManufacturer:fake,createProduct:fake,createVariant:fake,updateProduct:async()=>{},updateVariant:async()=>{},releaseVariantSkus:async()=>{}}}
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";export async function fetchAll(){return await db.selectFrom("categories").selectAll().execute()}export async function fetchById(id){return await db.selectFrom("categories").where("id","=",id).selectAll().executeTakeFirst()}export async function fetchByName(name){return await db.selectFrom("categories").where("name","=",name).selectAll().executeTakeFirst()}export async function fetchActive(){return await db.selectFrom("categories").where("is_active","=",!0).selectAll().execute()}export async function fetchRootCategories(){return await db.selectFrom("categories").whereNull("parent_category_id").where("is_active","=",!0).selectAll().execute()}export async function fetchChildCategories(parentId){return await db.selectFrom("categories").where("parent_category_id","=",parentId).where("is_active","=",!0).selectAll().execute()}export async function fetchByDisplayOrder(ascending=!0){const query=db.selectFrom("categories").where("is_active","=",!0).selectAll();if(ascending)return await query.orderBy("display_order","asc").execute();else return await query.orderBy("display_order","desc").execute()}export async function fetchStats(){const totalCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).executeTakeFirst(),activeCategories=await db.selectFrom("categories").where("is_active","=",!0).select(db.fn.count("id").as("count")).executeTakeFirst(),rootCategories=await db.selectFrom("categories").whereNull("parent_category_id").select(db.fn.count("id").as("count")).executeTakeFirst(),categoriesWithImages=await db.selectFrom("categories").whereNotNull("image_url").select(db.fn.count("id").as("count")).executeTakeFirst(),thirtyDaysAgo=new Date;thirtyDaysAgo.setDate(thirtyDaysAgo.getDate()-30);const thirtyDaysAgoStr=formatDate(thirtyDaysAgo),recentlyAddedCategories=await db.selectFrom("categories").where("created_at",">=",thirtyDaysAgoStr).selectAll().limit(5).execute(),categoriesByParent=await db.selectFrom("categories as c").leftJoin("categories as parent","c.parent_category_id","=","parent.id").whereNotNull("c.parent_category_id").select(["c.parent_category_id","parent.name as parent_name",db.fn.count("c.id").as("child_count")]).groupBy(["c.parent_category_id","parent.name"]).orderBy("child_count","desc").limit(5).execute();return{total:Number(totalCategories?.count||0),active:Number(activeCategories?.count||0),root_categories:Number(rootCategories?.count||0),child_categories:Number(totalCategories?.count||0)-Number(rootCategories?.count||0),with_images:Number(categoriesWithImages?.count||0),recently_added:recentlyAddedCategories,top_parent_categories:categoriesByParent.map((item)=>({id:String(item.parent_category_id||""),name:String(item.parent_name||""),child_count:Number(item.child_count)}))}}export async function compareCategoryGrowth(_daysRange=30){const today=new Date,todayStr=formatDate(today),currentPeriodStart=new Date;currentPeriodStart.setDate(today.getDate()-_daysRange);const currentPeriodStartStr=formatDate(currentPeriodStart),previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodEndStr=formatDate(previousPeriodEnd),previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-_daysRange);const previousPeriodStartStr=formatDate(previousPeriodStart),currentPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",currentPeriodStartStr).where("created_at","<=",todayStr).executeTakeFirst(),previousPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",previousPeriodStartStr).where("created_at","<=",previousPeriodEndStr).executeTakeFirst(),currentCount=Number(currentPeriodCategories?.count||0),previousCount=Number(previousPeriodCategories?.count||0),difference=currentCount-previousCount,percentageChange=previousCount!==0?difference/previousCount*100:currentCount>0?100:0;return{current_period:currentCount,previous_period:previousCount,difference,percentage_change:percentageChange,days_range:_daysRange}}export async function fetchCategoryTree(){const allCategories=await fetchAll(),categoryMap=new Map;allCategories.forEach((category)=>{categoryMap.set(category.id,{...category,children:[]})});const rootCategories=[];allCategories.forEach((category)=>{const categoryWithChildren=categoryMap.get(category.id);if(category.parent_category_id){const parent=categoryMap.get(category.parent_category_id);if(parent)parent.children.push(categoryWithChildren)}else rootCategories.push(categoryWithChildren)});rootCategories.sort((a,b)=>a.display_order-b.display_order);const sortChildrenByDisplayOrder=(categories)=>{categories.sort((a,b)=>a.display_order-b.display_order);categories.forEach((category)=>{if(category.children.length>0)sortChildrenByDisplayOrder(category.children)})};sortChildrenByDisplayOrder(rootCategories);return rootCategories}
1
+ import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";export async function fetchAll(){return await db.selectFrom("categories").selectAll().execute()}export async function fetchById(id){return await db.selectFrom("categories").where("id","=",id).selectAll().executeTakeFirst()}export async function fetchByName(name){return await db.selectFrom("categories").where("name","=",name).selectAll().executeTakeFirst()}export async function fetchActive(){return await db.selectFrom("categories").where("is_active","=",!0).selectAll().execute()}export async function fetchRootCategories(){return await db.selectFrom("categories").whereNull("parent_category_id").where("is_active","=",!0).selectAll().execute()}export async function fetchChildCategories(parentId){return await db.selectFrom("categories").where("parent_category_id","=",parentId).where("is_active","=",!0).selectAll().execute()}export async function fetchByDisplayOrder(ascending=!0){const query=db.selectFrom("categories").where("is_active","=",!0).selectAll();if(ascending)return await query.orderBy("display_order","asc").execute();else return await query.orderBy("display_order","desc").execute()}export async function fetchStats(){const totalCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).executeTakeFirst(),activeCategories=await db.selectFrom("categories").where("is_active","=",!0).select(db.fn.count("id").as("count")).executeTakeFirst(),rootCategories=await db.selectFrom("categories").whereNull("parent_category_id").select(db.fn.count("id").as("count")).executeTakeFirst(),categoriesWithImages=await db.selectFrom("categories").whereNotNull("image_url").select(db.fn.count("id").as("count")).executeTakeFirst(),thirtyDaysAgo=new Date;thirtyDaysAgo.setDate(thirtyDaysAgo.getDate()-30);const thirtyDaysAgoStr=formatDate(thirtyDaysAgo),recentlyAddedCategories=await db.selectFrom("categories").where("created_at",">=",thirtyDaysAgoStr).selectAll().limit(5).execute(),categoriesByParent=await db.selectFrom("categories as c").leftJoin("categories as parent","c.parent_category_id","=","parent.id").whereNotNull("c.parent_category_id").select(["c.parent_category_id","parent.name as parent_name",db.fn.count("c.id").as("child_count")]).groupBy("c.parent_category_id","parent.name").orderBy("child_count","desc").limit(5).execute();return{total:Number(totalCategories?.count||0),active:Number(activeCategories?.count||0),root_categories:Number(rootCategories?.count||0),child_categories:Number(totalCategories?.count||0)-Number(rootCategories?.count||0),with_images:Number(categoriesWithImages?.count||0),recently_added:recentlyAddedCategories,top_parent_categories:categoriesByParent.map((item)=>({id:String(item.parent_category_id||""),name:String(item.parent_name||""),child_count:Number(item.child_count)}))}}export async function compareCategoryGrowth(_daysRange=30){const today=new Date,todayStr=formatDate(today),currentPeriodStart=new Date;currentPeriodStart.setDate(today.getDate()-_daysRange);const currentPeriodStartStr=formatDate(currentPeriodStart),previousPeriodEnd=new Date(currentPeriodStart);previousPeriodEnd.setDate(previousPeriodEnd.getDate()-1);const previousPeriodEndStr=formatDate(previousPeriodEnd),previousPeriodStart=new Date(previousPeriodEnd);previousPeriodStart.setDate(previousPeriodEnd.getDate()-_daysRange);const previousPeriodStartStr=formatDate(previousPeriodStart),currentPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",currentPeriodStartStr).where("created_at","<=",todayStr).executeTakeFirst(),previousPeriodCategories=await db.selectFrom("categories").select(db.fn.count("id").as("count")).where("created_at",">=",previousPeriodStartStr).where("created_at","<=",previousPeriodEndStr).executeTakeFirst(),currentCount=Number(currentPeriodCategories?.count||0),previousCount=Number(previousPeriodCategories?.count||0),difference=currentCount-previousCount,percentageChange=previousCount!==0?difference/previousCount*100:currentCount>0?100:0;return{current_period:currentCount,previous_period:previousCount,difference,percentage_change:percentageChange,days_range:_daysRange}}export async function fetchCategoryTree(){const allCategories=await fetchAll(),categoryMap=new Map;allCategories.forEach((category)=>{categoryMap.set(category.id,{...category,children:[]})});const rootCategories=[];allCategories.forEach((category)=>{const categoryWithChildren=categoryMap.get(category.id);if(category.parent_category_id){const parent=categoryMap.get(category.parent_category_id);if(parent)parent.children.push(categoryWithChildren)}else rootCategories.push(categoryWithChildren)});rootCategories.sort((a,b)=>a.display_order-b.display_order);const sortChildrenByDisplayOrder=(categories)=>{categories.sort((a,b)=>a.display_order-b.display_order);categories.forEach((category)=>{if(category.children.length>0)sortChildrenByDisplayOrder(category.children)})};sortChildrenByDisplayOrder(rootCategories);return rootCategories}
@@ -1 +1 @@
1
- import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{slug}from"@stacksjs/strings";import{fetchById}from"./fetch";export async function store(data){try{const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);const categoryData={...data,uuid:randomUUIDv7(),is_active:data.is_active??!0,parent_category_id:data.parent_category_id||null},result=await db.insertInto("categories").values(categoryData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create category");return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to create category: ${error.message}`);throw error}}export async function findOrCreateByName(data){if(!data.name)throw Error("Name is required");const existingCategory=await db.selectFrom("categories").selectAll().where("name","=",data.name).executeTakeFirst();if(existingCategory)return existingCategory;const categoryData={name:data.name,slug:slug(data.name),is_active:data.is_active??!0,display_order:0};return await store(categoryData)}
1
+ import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{slug}from"@stacksjs/strings";import{fetchById}from"./fetch";export async function store(data){try{const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);const categoryData={...data,uuid:randomUUIDv7(),is_active:data.is_active??!0,parent_category_id:parentCategoryId??null},result=await db.insertInto("categories").values(categoryData).returningAll().executeTakeFirst();if(!result)throw Error("Failed to create category");return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to create category: ${error.message}`);throw error}}export async function findOrCreateByName(data){if(!data.name)throw Error("Name is required");const existingCategory=await db.selectFrom("categories").selectAll().where("name","=",data.name).executeTakeFirst();if(existingCategory)return existingCategory;const categoryData={name:data.name,slug:slug(data.name),is_active:data.is_active??!0,display_order:0};return await store(categoryData)}
@@ -4,7 +4,7 @@ export declare interface CategoryWriteData {
4
4
  slug?: string
5
5
  image_url?: string | null
6
6
  is_active?: boolean
7
- parent_category_id?: string | null
7
+ parent_category_id?: string | number | null
8
8
  display_order?: number
9
9
  uuid?: string
10
10
  }
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{fetchById}from"./fetch";export async function update(id,data){if(!await fetchById(id))return;if(Object.hasOwn(data,"parent_category_id")){const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId===id)throw new HttpError(422,"A category cannot be its own parent");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);if(parentCategoryId&&await wouldCreateCircularReference(id,String(parentCategoryId)))throw new HttpError(422,"This operation would create a circular category hierarchy")}try{const parentUpdate=Object.hasOwn(data,"parent_category_id")?{parent_category_id:data.parent_category_id||null}:{},result=await db.updateTable("categories").set({...data,...parentUpdate,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to update category: ${error.message}`);throw error}}export async function updateDisplayOrder(id,newOrder){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({display_order:newOrder,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category display order: ${error.message}`);throw error}}export async function updateActiveStatus(id,isActive){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({is_active:isActive,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category active status: ${error.message}`);throw error}}export async function updateParent(id,newParentId){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);if(newParentId){const newParentIdNum=Number(newParentId);if(newParentIdNum===id)throw Error("A category cannot be its own parent");if(!await fetchById(newParentIdNum))throw Error(`Parent category with ID ${newParentId} not found`);if(await wouldCreateCircularReference(id,newParentId))throw Error("This operation would create a circular reference in the category hierarchy")}try{const updateObject={updated_at:formatDate(new Date)};if(newParentId===null)await db.updateTable("categories").set({...updateObject,parent_category_id:null}).where("id","=",id).execute();else await db.updateTable("categories").set({...updateObject,parent_category_id:newParentId}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category parent: ${error.message}`);throw error}}async function wouldCreateCircularReference(categoryId,newParentId){let currentParentId=Number(newParentId);const visited=new Set;while(currentParentId){if(visited.has(currentParentId))return!0;if(currentParentId===categoryId)return!0;visited.add(currentParentId);const parent=await fetchById(currentParentId),nextParentId=parent?.parent_category_id;if(!parent||!nextParentId)return!1;currentParentId=Number(nextParentId)}return!1}
1
+ import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{fetchById}from"./fetch";export async function update(id,data){if(!await fetchById(id))return;const parentCategoryId=data.parent_category_id?Number(data.parent_category_id):void 0;if(Object.hasOwn(data,"parent_category_id")){if(parentCategoryId!==void 0&&(!Number.isSafeInteger(parentCategoryId)||parentCategoryId<=0))throw new HttpError(422,"Parent category ID must be a positive integer");if(parentCategoryId===id)throw new HttpError(422,"A category cannot be its own parent");if(parentCategoryId&&!await fetchById(parentCategoryId))throw new HttpError(422,`Parent category with ID ${data.parent_category_id} not found`);if(parentCategoryId&&await wouldCreateCircularReference(id,String(parentCategoryId)))throw new HttpError(422,"This operation would create a circular category hierarchy")}try{const parentUpdate=Object.hasOwn(data,"parent_category_id")?{parent_category_id:parentCategoryId??null}:{},result=await db.updateTable("categories").set({...data,...parentUpdate,updated_at:formatDate(new Date)}).where("id","=",id).returningAll().executeTakeFirst();if(!result)return;return result}catch(error){if(error instanceof HttpError)throw error;if(isUniqueViolation(error))throw new HttpError(409,"A category with this name or slug already exists");if(error instanceof Error)throw Error(`Failed to update category: ${error.message}`);throw error}}export async function updateDisplayOrder(id,newOrder){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({display_order:newOrder,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category display order: ${error.message}`);throw error}}export async function updateActiveStatus(id,isActive){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);try{await db.updateTable("categories").set({is_active:isActive,updated_at:formatDate(new Date)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category active status: ${error.message}`);throw error}}export async function updateParent(id,newParentId){if(!await fetchById(id))throw Error(`Category with ID ${id} not found`);if(newParentId){const newParentIdNum=Number(newParentId);if(newParentIdNum===id)throw Error("A category cannot be its own parent");if(!await fetchById(newParentIdNum))throw Error(`Parent category with ID ${newParentId} not found`);if(await wouldCreateCircularReference(id,newParentId))throw Error("This operation would create a circular reference in the category hierarchy")}try{const updateObject={updated_at:formatDate(new Date)};if(newParentId===null)await db.updateTable("categories").set({...updateObject,parent_category_id:null}).where("id","=",id).execute();else await db.updateTable("categories").set({...updateObject,parent_category_id:Number(newParentId)}).where("id","=",id).execute();return await fetchById(id)}catch(error){if(error instanceof Error)throw TypeError(`Failed to update category parent: ${error.message}`);throw error}}async function wouldCreateCircularReference(categoryId,newParentId){let currentParentId=Number(newParentId);const visited=new Set;while(currentParentId){if(visited.has(currentParentId))return!0;if(currentParentId===categoryId)return!0;visited.add(currentParentId);const parent=await fetchById(currentParentId),nextParentId=parent?.parent_category_id;if(!parent||!nextParentId)return!1;currentParentId=Number(nextParentId)}return!1}
@@ -1 +1 @@
1
- import{db,sql}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){const model=await db.selectFrom("products").where("id","=",id).selectAll().executeTakeFirst();if(model){let manufacturer=null,category=null;if(model.manufacturer_id)manufacturer=await db.selectFrom("manufacturers").where("id","=",model.manufacturer_id).selectAll().executeTakeFirst();if(model.category_id)category=await db.selectFrom("categories").where("id","=",model.category_id).selectAll().executeTakeFirst();return{...model,manufacturer,category}}return}export async function fetchAll(){const models=await db.selectFrom("products").selectAll().execute(),manufacturerIds=models.map((model)=>model.manufacturer_id).filter((id)=>id!==null&&id!==void 0),categoryIds=models.map((model)=>model.category_id).filter((id)=>id!==null&&id!==void 0);let manufacturersQuery=db.selectFrom("manufacturers"),categoriesQuery=db.selectFrom("categories");if(manufacturerIds.length>0)manufacturersQuery=manufacturersQuery.where("id","in",manufacturerIds);if(categoryIds.length>0)categoriesQuery=categoriesQuery.where("id","in",categoryIds);const allManufacturers=await manufacturersQuery.selectAll().execute(),allCategories=await categoriesQuery.selectAll().execute(),manufacturersById=allManufacturers.reduce((acc,manufacturer)=>{acc[manufacturer.id]=manufacturer;return acc},{}),categoriesById=allCategories.reduce((acc,category)=>{acc[category.id]=category;return acc},{});return models.map((model)=>({...model,manufacturer:model.manufacturer_id?manufacturersById[model.manufacturer_id]:null,category:model.category_id?categoriesById[model.category_id]:null}))}export async function getProductsByManufacturer(manufacturerId){try{const products=await db.selectFrom("products").selectAll().where("manufacturer_id","=",manufacturerId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by manufacturer: ${error.message}`);throw error}}export async function getProductsByCategory(categoryId){try{const products=await db.selectFrom("products").selectAll().where("category_id","=",categoryId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by category: ${error.message}`);throw error}}export async function getProductByUuid(uuid){try{const product=await db.selectFrom("products").selectAll().where("uuid","=",uuid).executeTakeFirst();return asModelRow(product,!0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get product by UUID: ${error.message}`);throw error}}export async function formatProductOptions(){try{return(await db.selectFrom("products").select(["id","name","uuid","price"]).orderBy("name").execute()).filter((result)=>result.name!==null&&result.name!==void 0&&result.uuid!==null&&result.uuid!==void 0&&result.price!==null&&result.price!==void 0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to format product options: ${error.message}`);throw error}}export async function getProductsByPriceRange(minPrice,maxPrice){try{const products=await db.selectFrom("products").selectAll().where("price",">=",minPrice).where("price","<=",maxPrice).orderBy("price").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by price range: ${error.message}`);throw error}}export async function fetchTopProducts(limit=5){try{return(await db.selectFrom("order_items").innerJoin("products","products.id","=","order_items.product_id").select(["products.id as id","products.name as name","products.inventory_count as inventory_count",db.fn.sum("order_items.quantity").as("units_sold"),sql`SUM(COALESCE(order_items.price, products.price) * order_items.quantity)`.as("revenue")]).groupBy(["products.id","products.name","products.inventory_count"]).orderBy("units_sold","desc").limit(limit).execute()).map((r)=>({id:Number(r.id),name:String(r.name??"Unnamed product"),units_sold:Number(r.units_sold??0),revenue:Number(r.revenue??0),inventory_count:Number(r.inventory_count??0)}))}catch(error){if(error instanceof Error)throw TypeError(`Failed to fetch top products: ${error.message}`);throw error}}
1
+ import{db,sql}from"@stacksjs/database/runtime";import{asModelRow,asModelRows}from"../../utils/model-row";export async function fetchById(id){const model=await db.selectFrom("products").where("id","=",id).selectAll().executeTakeFirst();if(model){let manufacturer=null,category=null;if(model.manufacturer_id)manufacturer=await db.selectFrom("manufacturers").where("id","=",model.manufacturer_id).selectAll().executeTakeFirst();if(model.category_id)category=await db.selectFrom("categories").where("id","=",model.category_id).selectAll().executeTakeFirst();return{...model,manufacturer,category}}return}export async function fetchAll(){const models=await db.selectFrom("products").selectAll().execute(),manufacturerIds=models.map((model)=>model.manufacturer_id).filter((id)=>id!==null&&id!==void 0),categoryIds=models.map((model)=>model.category_id).filter((id)=>id!==null&&id!==void 0);let manufacturersQuery=db.selectFrom("manufacturers"),categoriesQuery=db.selectFrom("categories");if(manufacturerIds.length>0)manufacturersQuery=manufacturersQuery.where("id","in",manufacturerIds);if(categoryIds.length>0)categoriesQuery=categoriesQuery.where("id","in",categoryIds);const allManufacturers=await manufacturersQuery.selectAll().execute(),allCategories=await categoriesQuery.selectAll().execute(),manufacturersById=allManufacturers.reduce((acc,manufacturer)=>{acc[manufacturer.id]=manufacturer;return acc},{}),categoriesById=allCategories.reduce((acc,category)=>{acc[category.id]=category;return acc},{});return models.map((model)=>({...model,manufacturer:model.manufacturer_id?manufacturersById[model.manufacturer_id]:null,category:model.category_id?categoriesById[model.category_id]:null}))}export async function getProductsByManufacturer(manufacturerId){try{const products=await db.selectFrom("products").selectAll().where("manufacturer_id","=",manufacturerId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by manufacturer: ${error.message}`);throw error}}export async function getProductsByCategory(categoryId){try{const products=await db.selectFrom("products").selectAll().where("category_id","=",categoryId).orderBy("name").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by category: ${error.message}`);throw error}}export async function getProductByUuid(uuid){try{const product=await db.selectFrom("products").selectAll().where("uuid","=",uuid).executeTakeFirst();return asModelRow(product,!0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get product by UUID: ${error.message}`);throw error}}export async function formatProductOptions(){try{return(await db.selectFrom("products").select(["id","name","uuid","price"]).orderBy("name").execute()).filter((result)=>result.name!==null&&result.name!==void 0&&result.uuid!==null&&result.uuid!==void 0&&result.price!==null&&result.price!==void 0)}catch(error){if(error instanceof Error)throw TypeError(`Failed to format product options: ${error.message}`);throw error}}export async function getProductsByPriceRange(minPrice,maxPrice){try{const products=await db.selectFrom("products").selectAll().where("price",">=",minPrice).where("price","<=",maxPrice).orderBy("price").execute();return asModelRows(products)}catch(error){if(error instanceof Error)throw TypeError(`Failed to get products by price range: ${error.message}`);throw error}}export async function fetchTopProducts(limit=5){try{return(await db.selectFrom("order_items").innerJoin("products","products.id","=","order_items.product_id").select(["products.id as id","products.name as name","products.inventory_count as inventory_count",db.fn.sum("order_items.quantity").as("units_sold"),sql`SUM(COALESCE(order_items.price, products.price) * order_items.quantity)`.as("revenue")]).groupBy("products.id","products.name","products.inventory_count").orderBy("units_sold","desc").limit(limit).execute()).map((r)=>({id:Number(r.id),name:String(r.name??"Unnamed product"),units_sold:Number(r.units_sold??0),revenue:Number(r.revenue??0),inventory_count:Number(r.inventory_count??0)}))}catch(error){if(error instanceof Error)throw TypeError(`Failed to fetch top products: ${error.message}`);throw error}}
@@ -1 +1 @@
1
- import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{insertedId}from"../../utils/inserted-id";import{fetchById,getProductByUuid}from"./fetch";export async function store(data){try{const itemData={...data,uuid:randomUUIDv7()},result=await db.insertInto("products").values(itemData).executeTakeFirst();if(!result)throw Error("Failed to create product item");const id=insertedId(result),model=id!==void 0?await fetchById(id):await getProductByUuid(itemData.uuid);if(!model)throw Error("Failed to create product item");return model}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product item: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{await db.transaction(async(trx)=>{for(const item of data){const itemData={...item,uuid:randomUUIDv7()};await trx.insertInto("products").values(itemData).execute();createdCount++}});return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product items in bulk: ${error.message}`);throw error}}
1
+ import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{uniqueSlug}from"@stacksjs/slug";import{insertedId}from"../../utils/inserted-id";import{fetchById,getProductByUuid}from"./fetch";async function productSlug(data,connection,claimed){const chosen=data.slug;if(typeof chosen==="string"&&chosen.trim()){claimed?.add(chosen.trim());return chosen.trim()}return uniqueSlug(String(data.name??""),{table:"products",column:"slug",connection,claimed})}export async function store(data){try{const itemData={...data,slug:await productSlug(data),uuid:randomUUIDv7()},result=await db.insertInto("products").values(itemData).executeTakeFirst();if(!result)throw Error("Failed to create product item");const id=insertedId(result),model=id!==void 0?await fetchById(id):await getProductByUuid(itemData.uuid);if(!model)throw Error("Failed to create product item");return model}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product item: ${error.message}`);throw error}}export async function bulkStore(data){if(!data.length)return 0;let createdCount=0;try{await db.transaction(async(trx)=>{const claimed=new Set;for(const item of data){const itemData={...item,slug:await productSlug(item,trx,claimed),uuid:randomUUIDv7()};await trx.insertInto("products").values(itemData).execute();createdCount++}});return createdCount}catch(error){if(error instanceof Error)throw TypeError(`Failed to create product items in bulk: ${error.message}`);throw error}}
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";const templates=["Standard License","Premium License","Enterprise License"],statuses=["active","inactive","unassigned"];export class LicenseKeyInputError extends TypeError{constructor(message){super(message);this.name="LicenseKeyInputError"}}function mergedValue(input,current,key){return input[key]!==void 0?input[key]:current?.[key]}function optionalId(input,current,key,label){const value=mergedValue(input,current,key);if(value===void 0||value===null||value==="")return null;const id=Number(value);if(!Number.isSafeInteger(id)||id<1)throw new LicenseKeyInputError(`${label} must be a positive integer or null.`);return id}function expiryTimestamp(value){if(typeof value==="number")return value>2147483647?Math.floor(value/1000):value;if(typeof value==="string"&&/^\d{10,13}$/.test(value))return value.length===13?Math.floor(Number(value)/1000):Number(value);if(typeof value==="string"){const normalized=/^\d{4}-\d{2}-\d{2} \d/.test(value)?`${value.replace(" ","T")}Z`:value;return Math.floor(new Date(normalized).getTime()/1000)}return Number.NaN}export async function validateLicenseKeyWrite(input,current){const key=String(mergedValue(input,current,"key")||"").trim().toUpperCase(),template=String(mergedValue(input,current,"template")||""),status=String(mergedValue(input,current,"status")||""),expiryDate=expiryTimestamp(mergedValue(input,current,"expiry_date")),customerId=optionalId(input,current,"customer_id","Customer"),productId=optionalId(input,current,"product_id","Product"),orderId=optionalId(input,current,"order_id","Order");if(!/^[A-Z0-9]{4}(?:-[A-Z0-9]{4}){4}$/.test(key))throw new LicenseKeyInputError("License key must use five groups of four uppercase letters or numbers.");if(!templates.includes(template))throw new LicenseKeyInputError(`Template must be one of: ${templates.join(", ")}.`);if(!statuses.includes(status))throw new LicenseKeyInputError(`Status must be one of: ${statuses.join(", ")}.`);if(!Number.isSafeInteger(expiryDate)||expiryDate<0||expiryDate>2147483647)throw new LicenseKeyInputError("Expiry date must be a Unix timestamp between 1970 and 2038.");const[customer,product,order]=await Promise.all([customerId?db.selectFrom("customers").where("id","=",customerId).select("id").executeTakeFirst():null,productId?db.selectFrom("products").where("id","=",productId).select("id").executeTakeFirst():null,orderId?db.selectFrom("orders").where("id","=",orderId).select(["id","customer_id"]).executeTakeFirst():null]);if(customerId&&!customer)throw new LicenseKeyInputError(`Customer ${customerId} was not found.`);if(productId&&!product)throw new LicenseKeyInputError(`Product ${productId} was not found.`);if(orderId&&!order)throw new LicenseKeyInputError(`Order ${orderId} was not found.`);if(customerId&&order?.customer_id&&Number(order.customer_id)!==customerId)throw new LicenseKeyInputError(`Order ${orderId} belongs to Customer ${order.customer_id}, not Customer ${customerId}.`);return{...input,key,template,expiry_date:expiryDate,status,customer_id:customerId,product_id:productId,order_id:orderId}}
1
+ import{db,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";const templates=["Standard License","Premium License","Enterprise License"],statuses=["active","inactive","unassigned"];export class LicenseKeyInputError extends TypeError{constructor(message){super(message);this.name="LicenseKeyInputError"}}function mergedValue(input,current,key){return input[key]!==void 0?input[key]:current?.[key]}function optionalId(input,current,key,label){const value=mergedValue(input,current,key);if(value===void 0||value===null||value==="")return null;const id=Number(value);if(!Number.isSafeInteger(id)||id<1)throw new LicenseKeyInputError(`${label} must be a positive integer or null.`);return id}function expiryTimestamp(value){if(value instanceof Date)return Math.floor(value.getTime()/1000);if(typeof value==="number")return value>2147483647?Math.floor(value/1000):value;if(typeof value==="string"&&/^\d{10,13}$/.test(value))return value.length===13?Math.floor(Number(value)/1000):Number(value);if(typeof value==="string"){const parsed=parseSqlDateTime(value);return parsed?Math.floor(parsed.getTime()/1000):Number.NaN}return Number.NaN}export async function validateLicenseKeyWrite(input,current){const key=String(mergedValue(input,current,"key")||"").trim().toUpperCase(),template=String(mergedValue(input,current,"template")||""),status=String(mergedValue(input,current,"status")||""),expiryDate=expiryTimestamp(mergedValue(input,current,"expiry_date")),customerId=optionalId(input,current,"customer_id","Customer"),productId=optionalId(input,current,"product_id","Product"),orderId=optionalId(input,current,"order_id","Order");if(!/^[A-Z0-9]{4}(?:-[A-Z0-9]{4}){4}$/.test(key))throw new LicenseKeyInputError("License key must use five groups of four uppercase letters or numbers.");if(!templates.includes(template))throw new LicenseKeyInputError(`Template must be one of: ${templates.join(", ")}.`);if(!statuses.includes(status))throw new LicenseKeyInputError(`Status must be one of: ${statuses.join(", ")}.`);if(!Number.isSafeInteger(expiryDate)||expiryDate<0||expiryDate>2147483647)throw new LicenseKeyInputError("Expiry date must be a Unix timestamp between 1970 and 2038.");const[customer,product,order]=await Promise.all([customerId?db.selectFrom("customers").where("id","=",customerId).select("id").executeTakeFirst():null,productId?db.selectFrom("products").where("id","=",productId).select("id").executeTakeFirst():null,orderId?db.selectFrom("orders").where("id","=",orderId).select(["id","customer_id"]).executeTakeFirst():null]);if(customerId&&!customer)throw new LicenseKeyInputError(`Customer ${customerId} was not found.`);if(productId&&!product)throw new LicenseKeyInputError(`Product ${productId} was not found.`);if(orderId&&!order)throw new LicenseKeyInputError(`Order ${orderId} was not found.`);if(customerId&&order?.customer_id&&Number(order.customer_id)!==customerId)throw new LicenseKeyInputError(`Order ${orderId} belongs to Customer ${order.customer_id}, not Customer ${customerId}.`);return{...input,key,template,expiry_date:sqlDateTime(new Date(expiryDate*1000)),status,customer_id:customerId,product_id:productId,order_id:orderId}}
@@ -1 +1 @@
1
- import assert from"node:assert/strict";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_RETURNING_ALL_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-returning-all-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_returning_all_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","categories","product_units","product_variants","products","shipping_methods","shipping_rates","shipping_zones"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),{update:updateCart}=await import("../../carts/update"),{update:updateItem}=await import("../../products/items/update"),{update:updateVariant}=await import("../../products/variants/update"),{update:updateUnit}=await import("../../products/units/update"),{update:updateRate}=await import("../../shippings/shipping-rates/update"),{store:storeCategory}=await import("../../products/categories/store");async function stored(table,id){return await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst()}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();await db.insertInto("carts").values({id:1,status:"active",expires_at:"2030-02-01 00:00:00",applied_coupon_id:"none"}).execute();await db.insertInto("products").values({id:1,name:"before",price:100,preparation_time:5}).execute();await db.insertInto("product_variants").values({id:1,variant:"Size",type:"dropdown",status:"active"}).execute();await db.insertInto("product_units").values({id:1,name:"gram",abbreviation:"g",type:"weight"}).execute();await db.insertInto("shipping_methods").values({id:1,name:"standard",base_rate:500,status:"active"}).execute();await db.insertInto("shipping_zones").values({id:1,name:"domestic",status:"active",shipping_method_id:1}).execute();await db.insertInto("shipping_rates").values({id:1,weight_from:0,weight_to:10,rate:100,shipping_method_id:1,shipping_zone_id:1}).execute();await check("carts update",async()=>{const row=await updateCart(1,{notes:"after"});assert.equal(row?.notes,"after",`${dialect}: the returned cart`);assert.equal((await stored("carts",1))?.notes,"after",`${dialect}: the stored cart`)});await check("product items update",async()=>{const row=await updateItem(1,{name:"after"});assert.equal(row?.name,"after",`${dialect}: the returned product`);assert.equal((await stored("products",1))?.name,"after",`${dialect}: the stored product`)});await check("product variants update",async()=>{const row=await updateVariant(1,{variant:"Colour"});assert.equal(row?.variant,"Colour",`${dialect}: the returned variant`);assert.equal((await stored("product_variants",1))?.variant,"Colour",`${dialect}: the stored variant`)});await check("product units update",async()=>{const row=await updateUnit(1,{name:"kilogram"});assert.equal(row?.name,"kilogram",`${dialect}: the returned unit`);assert.equal((await stored("product_units",1))?.name,"kilogram",`${dialect}: the stored unit`)});await check("shipping rates update",async()=>{const row=await updateRate(1,{rate:250});assert.equal(Number(row?.rate),250,`${dialect}: the returned rate`);assert.equal(Number((await stored("shipping_rates",1))?.rate),250,`${dialect}: the stored rate`)});await check("categories store",async()=>{const row=await storeCategory({name:"Shoes",slug:`shoes-${dialect}`,display_order:1});assert(row?.id,`${dialect}: the inserted category comes back with its id`);assert.equal(row.name,"Shoes");assert.equal((await stored("categories",row.id))?.slug,`shoes-${dialect}`,`${dialect}: the stored category`)});assert.deepEqual(failures,[],`${dialect}: every returningAll() write must apply and return its row`);console.log("commerce returningAll writes OK")}finally{resetDatabaseConnection()}
1
+ import assert from"node:assert/strict";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_RETURNING_ALL_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-returning-all-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_returning_all_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","categories","customers","product_units","product_variants","products","shipping_methods","shipping_rates","shipping_zones"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),{update:updateCart}=await import("../../carts/update"),{update:updateItem}=await import("../../products/items/update"),{update:updateVariant}=await import("../../products/variants/update"),{update:updateUnit}=await import("../../products/units/update"),{update:updateRate}=await import("../../shippings/shipping-rates/update"),{store:storeCategory}=await import("../../products/categories/store"),{store:storeCustomer}=await import("../../customers/store"),{bulkStore:bulkStoreItems,store:storeItem}=await import("../../products/items/store");async function stored(table,id){return await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst()}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();await db.insertInto("carts").values({id:1,status:"active",expires_at:"2030-02-01 00:00:00",applied_coupon_id:"none"}).execute();await db.insertInto("products").values({id:1,name:"before",slug:"before",price:100,preparation_time:5}).execute();await db.insertInto("product_variants").values({id:1,variant:"Size",type:"dropdown",status:"active"}).execute();await db.insertInto("product_units").values({id:1,name:"gram",abbreviation:"g",type:"weight"}).execute();await db.insertInto("shipping_methods").values({id:1,name:"standard",base_rate:500,status:"active"}).execute();await db.insertInto("shipping_zones").values({id:1,name:"domestic",status:"active",shipping_method_id:1}).execute();await db.insertInto("shipping_rates").values({id:1,weight_from:0,weight_to:10,rate:100,shipping_method_id:1,shipping_zone_id:1}).execute();if(dialect==="postgres")await db.unsafe(`SELECT setval(pg_get_serial_sequence('products', 'id'), (SELECT MAX("id") FROM "products"))`).execute();await check("carts update",async()=>{const row=await updateCart(1,{notes:"after"});assert.equal(row?.notes,"after",`${dialect}: the returned cart`);assert.equal((await stored("carts",1))?.notes,"after",`${dialect}: the stored cart`)});await check("product items update",async()=>{const row=await updateItem(1,{name:"after"});assert.equal(row?.name,"after",`${dialect}: the returned product`);assert.equal((await stored("products",1))?.name,"after",`${dialect}: the stored product`)});await check("product variants update",async()=>{const row=await updateVariant(1,{variant:"Colour"});assert.equal(row?.variant,"Colour",`${dialect}: the returned variant`);assert.equal((await stored("product_variants",1))?.variant,"Colour",`${dialect}: the stored variant`)});await check("product units update",async()=>{const row=await updateUnit(1,{name:"kilogram"});assert.equal(row?.name,"kilogram",`${dialect}: the returned unit`);assert.equal((await stored("product_units",1))?.name,"kilogram",`${dialect}: the stored unit`)});await check("shipping rates update",async()=>{const row=await updateRate(1,{rate:250});assert.equal(Number(row?.rate),250,`${dialect}: the returned rate`);assert.equal(Number((await stored("shipping_rates",1))?.rate),250,`${dialect}: the stored rate`)});await check("categories store",async()=>{const row=await storeCategory({name:"Shoes",slug:`shoes-${dialect}`,display_order:1});assert(row?.id,`${dialect}: the inserted category comes back with its id`);assert.equal(row.name,"Shoes");assert.equal((await stored("categories",row.id))?.slug,`shoes-${dialect}`,`${dialect}: the stored category`)});await check("customers store",async()=>{const row=await storeCustomer({name:"Ada",email:`ada-${dialect}@example.test`,phone:"+15555550100",status:"Active"});assert(row?.id,`${dialect}: the stored customer comes back with its id`);assert.equal((await stored("customers",row.id))?.email,`ada-${dialect}@example.test`,`${dialect}: the stored customer`)});await check("product items store gives each product a slug",async()=>{const first=await storeItem({name:"Trail Runner",price:100,preparation_time:5}),second=await storeItem({name:"Trail Runner",price:100,preparation_time:5});assert.equal(first?.slug,"trail-runner",`${dialect}: the first product's slug`);assert.equal(second?.slug,"trail-runner-2",`${dialect}: a second product with the same name`);const named=await storeItem({name:"Desk Lamp",slug:"reading-lamp",price:100,preparation_time:5});assert.equal(named?.slug,"reading-lamp",`${dialect}: a slug the caller chose is kept`);assert.equal(await bulkStoreItems([{name:"Trail Runner",price:100,preparation_time:5},{name:"Trail Runner",price:100,preparation_time:5}]),2,`${dialect}: both bulk products are written`);const slugs=(await db.selectFrom("products").select("slug").where("name","=","Trail Runner").execute()).map((row)=>row.slug).sort();assert.deepEqual(slugs,["trail-runner","trail-runner-2","trail-runner-3","trail-runner-4"],`${dialect}: every Trail Runner has its own slug`)});assert.deepEqual(failures,[],`${dialect}: every returningAll() write must apply and return its row`);console.log("commerce returningAll writes OK")}finally{resetDatabaseConnection()}
@@ -1 +1 @@
1
- import assert from"node:assert/strict";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_SOFT_DELETE_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-soft-delete-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_soft_delete_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","digital_deliveries","gift_cards","license_keys","orders","product_units","product_variants","products","shipping_methods","shipping_rates","shipping_zones"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),orders=await import("../../orders/destroy"),digitalDeliveries=await import("../../shippings/digital-deliveries/destroy"),licenseKeys=await import("../../shippings/license-keys/destroy"),shippingMethods=await import("../../shippings/shipping-methods/destroy"),shippingZones=await import("../../shippings/shipping-zones/destroy"),{deactivate:deactivateGiftCard}=await import("../../gift-cards/destroy"),{bulkUpdate:bulkUpdateVariants,updateStatus:updateVariantStatus}=await import("../../products/variants/update"),{bulkUpdate:bulkUpdateCarts}=await import("../../carts/update"),{updateByZone:updateRatesByZone}=await import("../../shippings/shipping-rates/update"),{bulkUpdate:bulkUpdateItems}=await import("../../products/items/update"),{bulkUpdate:bulkUpdateUnits}=await import("../../products/units/update"),MODULES=[{name:"orders",table:"orders",deleted:"CANCELED",module:orders,row:(id,status)=>({id,status,total_amount:1000,order_type:"DELIVERY"}),live:"PENDING"},{name:"shippings/digital-deliveries",table:"digital_deliveries",deleted:"inactive",module:digitalDeliveries,row:(id,status)=>({id,status,name:`delivery-${id}`,description:"seeded",expiry_days:7}),live:"active"},{name:"shippings/license-keys",table:"license_keys",deleted:"inactive",module:licenseKeys,row:(id,status)=>({id,status,key:`KEY-${id}`,template:"Standard License",expiry_date:"2030-01-01"}),live:"active"},{name:"shippings/shipping-methods",table:"shipping_methods",deleted:"inactive",module:shippingMethods,row:(id,status)=>({id,status,name:`method-${id}`,base_rate:500}),live:"active"},{name:"shippings/shipping-zones",table:"shipping_zones",deleted:"inactive",module:shippingZones,row:(id,status)=>({id,status,name:`zone-${id}`}),live:"active"}];async function statusOf(table,id){return(await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst())?.status}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();for(const{name,table,deleted,live,module,row}of MODULES)await check(`${name} soft delete counts rows already in the target state`,async()=>{for(const[id,status]of[[1,deleted],[2,live],[3,deleted],[4,live]])await db.insertInto(table).values(row(id,status)).execute();assert.equal(await module.softDelete(1),!0,`${dialect}: soft-deleting an already soft-deleted row must report success`);assert.equal(await statusOf(table,1),deleted,"the row is still there, still soft-deleted");assert.equal(await module.softDelete(2),!0,`${dialect}: soft-deleting a live row must report success`);assert.equal(await statusOf(table,2),deleted);assert.equal(await module.bulkSoftDelete([3,4]),2,`${dialect}: bulk soft delete must count rows already in the target state`);assert.equal(await statusOf(table,3),deleted);assert.equal(await statusOf(table,4),deleted);assert.equal(await module.softDelete(404),!1,`${dialect}: soft-deleting a row that does not exist must report failure`);assert.equal(await module.bulkSoftDelete([404,405]),0)});const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{await check("gift card deactivate is idempotent within one second",async()=>{await db.insertInto("gift_cards").values({id:1,code:"GC-1",initial_balance:5000,current_balance:5000,status:"ACTIVE"}).execute();assert.equal(await deactivateGiftCard(1),!0,`${dialect}: deactivating an active gift card must report success`);assert.equal(await deactivateGiftCard(1),!0,`${dialect}: deactivating an already deactivated gift card must report success`);assert.equal(await statusOf("gift_cards",1),"DEACTIVATED")});await check("variant updateStatus reports success for the status it already has",async()=>{await db.insertInto("product_variants").values({id:1,variant:"Size",type:"dropdown",status:"active"}).execute();await db.insertInto("product_variants").values({id:2,variant:"Colour",type:"dropdown",status:"draft"}).execute();assert.equal(await updateVariantStatus(1,"active"),!0,`${dialect}: re-applying a variant's current status must report success`);assert.equal(await updateVariantStatus(1,"active"),!0,`${dialect}: re-applying it again in the same second must report success`);assert.equal(await updateVariantStatus(2,"active"),!0,`${dialect}: changing a variant's status must report success`);assert.equal(await statusOf("product_variants",2),"active");assert.equal(await updateVariantStatus(404,"active"),!1,`${dialect}: a variant that does not exist must report failure`)});await check("variant bulkUpdate counts rows already holding the values",async()=>{const rows=await db.selectFrom("product_variants").selectAll().where("id","in",[1,2]).execute();assert.equal(rows.length,2);const resaved=await bulkUpdateVariants(rows.map((row)=>({id:row.id,status:row.status})));assert.equal(resaved,2,`${dialect}: bulk update must count rows that already hold the values written`)});await check("cart bulkUpdate counts rows already holding the values, inside a transaction",async()=>{for(const id of[1,2])await db.insertInto("carts").values({id,status:"active",expires_at:"2030-02-01 00:00:00",applied_coupon_id:"none"}).execute();const rows=[{id:1,status:"active"},{id:2,status:"active"}];assert.equal(await bulkUpdateCarts(rows),2);assert.equal(await bulkUpdateCarts(rows),2,`${dialect}: bulk update must count carts that already hold the values written`)});await check("shipping rate updateByZone counts rates already at that rate",async()=>{for(const id of[1,2])await db.insertInto("shipping_rates").values({id,weight_from:0,weight_to:10,rate:250,shipping_zone_id:7}).execute();assert.equal(await updateRatesByZone(7,{rate:250}),2);assert.equal(await updateRatesByZone(7,{rate:250}),2,`${dialect}: updateByZone must count rates already at the requested rate`)});await check("product bulkUpdate counts rows already holding the values",async()=>{for(const id of[1,2])await db.insertInto("products").values({id,name:`product-${id}`,price:100,preparation_time:5}).execute();const rows=[{id:1,name:"product-1"},{id:2,name:"product-2"}];assert.equal(await bulkUpdateItems(rows),2);assert.equal(await bulkUpdateItems(rows),2,`${dialect}: bulk update must count products that already hold the values written`)});await check("product unit bulkUpdate counts rows already holding the values",async()=>{for(const id of[1,2])await db.insertInto("product_units").values({id,name:`unit-${id}`,abbreviation:`u${id}`,type:"weight"}).execute();const rows=[{id:1,name:"unit-1"},{id:2,name:"unit-2"}];assert.equal(await bulkUpdateUnits(rows),2);assert.equal(await bulkUpdateUnits(rows),2,`${dialect}: bulk update must count units that already hold the values written`)})}finally{setSystemTime()}assert.deepEqual(failures,[],`${dialect}: every write must report the rows its predicate matched`);console.log("commerce soft delete counts OK")}finally{resetDatabaseConnection()}
1
+ import assert from"node:assert/strict";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_SOFT_DELETE_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-soft-delete-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_soft_delete_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","digital_deliveries","gift_cards","license_keys","orders","product_units","product_variants","products","shipping_methods","shipping_rates","shipping_zones"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),orders=await import("../../orders/destroy"),digitalDeliveries=await import("../../shippings/digital-deliveries/destroy"),licenseKeys=await import("../../shippings/license-keys/destroy"),shippingMethods=await import("../../shippings/shipping-methods/destroy"),shippingZones=await import("../../shippings/shipping-zones/destroy"),{deactivate:deactivateGiftCard}=await import("../../gift-cards/destroy"),{bulkUpdate:bulkUpdateVariants,updateStatus:updateVariantStatus}=await import("../../products/variants/update"),{bulkUpdate:bulkUpdateCarts}=await import("../../carts/update"),{updateByZone:updateRatesByZone}=await import("../../shippings/shipping-rates/update"),{bulkUpdate:bulkUpdateItems}=await import("../../products/items/update"),{bulkUpdate:bulkUpdateUnits}=await import("../../products/units/update"),MODULES=[{name:"orders",table:"orders",deleted:"CANCELED",module:orders,row:(id,status)=>({id,status,total_amount:1000,order_type:"DELIVERY"}),live:"PENDING"},{name:"shippings/digital-deliveries",table:"digital_deliveries",deleted:"inactive",module:digitalDeliveries,row:(id,status)=>({id,status,name:`delivery-${id}`,description:"seeded",expiry_days:7}),live:"active"},{name:"shippings/license-keys",table:"license_keys",deleted:"inactive",module:licenseKeys,row:(id,status)=>({id,status,key:`KEY-${id}`,template:"Standard License",expiry_date:"2030-01-01"}),live:"active"},{name:"shippings/shipping-methods",table:"shipping_methods",deleted:"inactive",module:shippingMethods,row:(id,status)=>({id,status,name:`method-${id}`,base_rate:500}),live:"active"},{name:"shippings/shipping-zones",table:"shipping_zones",deleted:"inactive",module:shippingZones,row:(id,status)=>({id,status,name:`zone-${id}`}),live:"active"}];async function statusOf(table,id){return(await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst())?.status}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();for(const{name,table,deleted,live,module,row}of MODULES)await check(`${name} soft delete counts rows already in the target state`,async()=>{for(const[id,status]of[[1,deleted],[2,live],[3,deleted],[4,live]])await db.insertInto(table).values(row(id,status)).execute();assert.equal(await module.softDelete(1),!0,`${dialect}: soft-deleting an already soft-deleted row must report success`);assert.equal(await statusOf(table,1),deleted,"the row is still there, still soft-deleted");assert.equal(await module.softDelete(2),!0,`${dialect}: soft-deleting a live row must report success`);assert.equal(await statusOf(table,2),deleted);assert.equal(await module.bulkSoftDelete([3,4]),2,`${dialect}: bulk soft delete must count rows already in the target state`);assert.equal(await statusOf(table,3),deleted);assert.equal(await statusOf(table,4),deleted);assert.equal(await module.softDelete(404),!1,`${dialect}: soft-deleting a row that does not exist must report failure`);assert.equal(await module.bulkSoftDelete([404,405]),0)});const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{await check("gift card deactivate is idempotent within one second",async()=>{await db.insertInto("gift_cards").values({id:1,code:"GC-1",initial_balance:5000,current_balance:5000,status:"ACTIVE"}).execute();assert.equal(await deactivateGiftCard(1),!0,`${dialect}: deactivating an active gift card must report success`);assert.equal(await deactivateGiftCard(1),!0,`${dialect}: deactivating an already deactivated gift card must report success`);assert.equal(await statusOf("gift_cards",1),"DEACTIVATED")});await check("variant updateStatus reports success for the status it already has",async()=>{await db.insertInto("product_variants").values({id:1,variant:"Size",type:"dropdown",status:"active"}).execute();await db.insertInto("product_variants").values({id:2,variant:"Colour",type:"dropdown",status:"draft"}).execute();assert.equal(await updateVariantStatus(1,"active"),!0,`${dialect}: re-applying a variant's current status must report success`);assert.equal(await updateVariantStatus(1,"active"),!0,`${dialect}: re-applying it again in the same second must report success`);assert.equal(await updateVariantStatus(2,"active"),!0,`${dialect}: changing a variant's status must report success`);assert.equal(await statusOf("product_variants",2),"active");assert.equal(await updateVariantStatus(404,"active"),!1,`${dialect}: a variant that does not exist must report failure`)});await check("variant bulkUpdate counts rows already holding the values",async()=>{const rows=await db.selectFrom("product_variants").selectAll().where("id","in",[1,2]).execute();assert.equal(rows.length,2);const resaved=await bulkUpdateVariants(rows.map((row)=>({id:row.id,status:row.status})));assert.equal(resaved,2,`${dialect}: bulk update must count rows that already hold the values written`)});await check("cart bulkUpdate counts rows already holding the values, inside a transaction",async()=>{for(const id of[1,2])await db.insertInto("carts").values({id,status:"active",expires_at:"2030-02-01 00:00:00",applied_coupon_id:"none"}).execute();const rows=[{id:1,status:"active"},{id:2,status:"active"}];assert.equal(await bulkUpdateCarts(rows),2);assert.equal(await bulkUpdateCarts(rows),2,`${dialect}: bulk update must count carts that already hold the values written`)});await check("shipping rate updateByZone counts rates already at that rate",async()=>{for(const id of[1,2])await db.insertInto("shipping_rates").values({id,weight_from:0,weight_to:10,rate:250,shipping_zone_id:7}).execute();assert.equal(await updateRatesByZone(7,{rate:250}),2);assert.equal(await updateRatesByZone(7,{rate:250}),2,`${dialect}: updateByZone must count rates already at the requested rate`)});await check("product bulkUpdate counts rows already holding the values",async()=>{for(const id of[1,2])await db.insertInto("products").values({id,name:`product-${id}`,slug:`product-${id}`,price:100,preparation_time:5}).execute();const rows=[{id:1,name:"product-1"},{id:2,name:"product-2"}];assert.equal(await bulkUpdateItems(rows),2);assert.equal(await bulkUpdateItems(rows),2,`${dialect}: bulk update must count products that already hold the values written`)});await check("product unit bulkUpdate counts rows already holding the values",async()=>{for(const id of[1,2])await db.insertInto("product_units").values({id,name:`unit-${id}`,abbreviation:`u${id}`,type:"weight"}).execute();const rows=[{id:1,name:"unit-1"},{id:2,name:"unit-2"}];assert.equal(await bulkUpdateUnits(rows),2);assert.equal(await bulkUpdateUnits(rows),2,`${dialect}: bulk update must count units that already hold the values written`)})}finally{setSystemTime()}assert.deepEqual(failures,[],`${dialect}: every write must report the rows its predicate matched`);console.log("commerce soft delete counts OK")}finally{resetDatabaseConnection()}
@@ -1,8 +1,8 @@
1
- import assert from"node:assert/strict";import{tmpdir}from"node:os";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_WRITE_COUNTS_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-write-counts-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_write_counts_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{Database}=await import("bun:sqlite"),{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection,sqlDateTime}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","categories","coupons","gift_cards","payments","receipts"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),{deactivate,deactivateChildCategories}=await import("../../products/categories/destroy"),{registerPersistentQueryHooks}=await import("@stacksjs/query-builder"),{redeem}=await import("../../coupons/update"),{store:storeCoupon}=await import("../../coupons/store"),{updateBalance}=await import("../../gift-cards/update"),{store:storeGiftCard}=await import("../../gift-cards/store"),{cleanupAbandonedCarts}=await import("../../orders/guards"),{recordRefund}=await import("../../payments/update"),{store:storePayment}=await import("../../payments/store"),{bulkStore}=await import("../../receipts/store");async function count(table){return(await db.selectFrom(table).selectAll().execute()).length}async function column(table,id,name){return(await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst())?.[name]}function deleteOnAnotherConnection(id){if(dialect==="sqlite"){const handle=new Database(process.env.DB_DATABASE_PATH);try{handle.run("DELETE FROM categories WHERE id = ?",[id])}finally{handle.close()}return!0}const adapter=dialect==="postgres"?"postgres":"mysql",child=Bun.spawnSync([process.execPath,"-e",`
1
+ import assert from"node:assert/strict";import{tmpdir}from"node:os";import{basename,dirname,join}from"node:path";import process from"node:process";const dialect=process.env.DB_CONNECTION;assert(dialect==="sqlite"||dialect==="postgres"||dialect==="mysql");const configPath=process.env.STACKS_COMMERCE_WRITE_COUNTS_CONFIG;assert(configPath&&basename(dirname(configPath)).startsWith("stacks-commerce-write-counts-"));if(dialect==="sqlite")assert.equal(dirname(process.env.DB_DATABASE_PATH),dirname(configPath));else{assert.equal(process.env.DB_DATABASE_PATH,":memory:");assert(process.env.DB_DATABASE?.startsWith("stacks_commerce_write_counts_"));assert(["127.0.0.1","localhost","[::1]"].includes(process.env.DB_HOST))}const{overridesReady}=await import("@stacksjs/config");await overridesReady;const{Database}=await import("bun:sqlite"),{db,ensureDatabaseConfigLoaded,initializeDbConfig,resetDatabaseConnection,sqlDateTime}=await import("@stacksjs/database");await ensureDatabaseConfigLoaded();initializeDbConfig({app:{env:"test"},database:{default:dialect,connections:dialect==="sqlite"?{sqlite:{database:process.env.DB_DATABASE_PATH}}:{[dialect]:{name:process.env.DB_DATABASE,host:process.env.DB_HOST,port:Number(process.env.DB_PORT),username:process.env.DB_USERNAME,password:process.env.DB_PASSWORD}},queryLogging:{enabled:!1}}});const TABLES=["carts","categories","coupons","gift_cards","license_keys","payments","receipts"],{buildMigrationPlan,generateSql,loadModels}=await import("bun-query-builder"),modelsDir=join(import.meta.dir,"../../../../../defaults/app/Models"),models={...await loadModels({modelsDir}),...await loadModels({modelsDir:join(modelsDir,"commerce")})},plan=buildMigrationPlan(models,{dialect});plan.tables=plan.tables.filter((table)=>TABLES.includes(table.table));assert.deepEqual(plan.tables.map((table)=>table.table).sort(),TABLES);for(const table of plan.tables)for(const column of table.columns)delete column.references;const statements=generateSql(plan),{deactivate,deactivateChildCategories}=await import("../../products/categories/destroy"),{fetchStats:fetchCategoryStats}=await import("../../products/categories/fetch"),{registerPersistentQueryHooks}=await import("@stacksjs/query-builder"),{redeem}=await import("../../coupons/update"),{store:storeCoupon}=await import("../../coupons/store"),{updateBalance}=await import("../../gift-cards/update"),{store:storeGiftCard}=await import("../../gift-cards/store"),{update:updateGiftCard}=await import("../../gift-cards/update"),{store:storeLicenseKey}=await import("../../shippings/license-keys/store"),{update:updateLicenseKey}=await import("../../shippings/license-keys/update"),{parseSqlDateTime}=await import("@stacksjs/database"),{cleanupAbandonedCarts}=await import("../../orders/guards"),{recordRefund}=await import("../../payments/update"),{store:storePayment}=await import("../../payments/store"),{bulkStore}=await import("../../receipts/store");async function count(table){return(await db.selectFrom(table).selectAll().execute()).length}async function column(table,id,name){return(await db.selectFrom(table).selectAll().where("id","=",id).executeTakeFirst())?.[name]}function deleteOnAnotherConnection(id){if(dialect==="sqlite"){const handle=new Database(process.env.DB_DATABASE_PATH);try{handle.run("DELETE FROM categories WHERE id = ?",[id])}finally{handle.close()}return!0}const adapter=dialect==="postgres"?"postgres":"mysql",child=Bun.spawnSync([process.execPath,"-e",`
2
2
  const db = new Bun.SQL({ adapter: '${adapter}', hostname: process.env.DB_HOST,
3
3
  port: Number(process.env.DB_PORT), database: process.env.DB_DATABASE,
4
4
  username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD,
5
5
  tls: process.env.DB_SSL === 'true' ? 'require' : 'disable' })
6
6
  try { await db.unsafe('DELETE FROM categories WHERE id = ${id}') }
7
7
  finally { await db.close() }
8
- `],{cwd:tmpdir(),env:process.env,stdout:"pipe",stderr:"pipe",timeout:5000});if(child.exitCode!==0)console.error(new TextDecoder().decode(child.stderr));return child.exitCode===0}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();const today=new Date;await check("orders cleanupAbandonedCarts",async()=>{const old="2020-01-01 00:00:00",fresh=today.toISOString().slice(0,19).replace("T"," ");for(const[index,updatedAt]of[old,old,fresh].entries())await db.insertInto("carts").values({uuid:`cart-${index}`,applied_coupon_id:"",expires_at:updatedAt,created_at:updatedAt,updated_at:updatedAt}).execute();const before=await count("carts"),{deleted}=await cleanupAbandonedCarts({olderThanDays:30}),removed=before-await count("carts");assert.equal(removed,2,"the two abandoned carts were deleted");assert.equal(deleted,removed,`cleanupAbandonedCarts reported ${deleted} but removed ${removed} carts`)});await check("coupons redeem",async()=>{const today=new Date,day=(offset)=>new Date(today.getTime()+offset*86400000).toISOString().slice(0,10),coupon=await storeCoupon({code:`REDEEM-${dialect}`,discount_type:"percentage",discount_value:10,product_id:1,start_date:day(-1),end_date:day(30),is_active:!0,usage_limit:2});assert(coupon?.id,"seed coupon");const id=Number(coupon.id),first=await redeem(id);assert.equal(Number(await column("coupons",id,"usage_count")),1,"the redemption was committed");assert.equal(first.ok,!0,`redeem reported ${JSON.stringify(first)} for a redemption it committed`);assert.equal((await redeem(id)).ok,!0);const third=await redeem(id);assert.equal(third.ok,!1);assert.equal(third.ok===!1&&third.reason,"limit-reached");assert.equal(Number(await column("coupons",id,"usage_count")),2,"a rejected redemption must not count")});await check("gift cards updateBalance",async()=>{const card=await storeGiftCard({code:`GC-WRITE-COUNTS-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0});assert(card?.id,"seed gift card");const spent=await updateBalance(Number(card.id),-40);assert.equal(Number(await column("gift_cards",card.id,"current_balance")),60);assert.equal(Number(spent?.current_balance),60);const used=await updateBalance(Number(card.id),-60);assert.equal(await column("gift_cards",card.id,"status"),"USED");assert.equal(used?.status,"USED");await assert.rejects(updateBalance(Number(card.id),-1),/not active/,"a spent card rejects another redemption")});await check("gift cards updateBalance by zero",async()=>{const card=await storeGiftCard({code:`GC-ZERO-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0});assert(card?.id,"seed gift card");const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{await updateBalance(Number(card.id),0);const again=await updateBalance(Number(card.id),0);assert.equal(Number(again?.current_balance),100,`${dialect}: a zero adjustment must return the card unchanged`);await assert.rejects(updateBalance(Number(card.id),-1000),/Insufficient/,"an over-spend is still insufficient");assert.equal(Number(await column("gift_cards",card.id,"current_balance")),100)}finally{setSystemTime()}});await check("gift cards updateBalance on an expired card",async()=>{const card=await storeGiftCard({code:`GC-EXPIRED-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0,expiry_date:"2020-01-01 00:00:00"});assert(card?.id,"seed gift card");await assert.rejects(updateBalance(Number(card.id),-10),/expired/,`${dialect}: a redemption on an expired card must say it expired`);await assert.rejects(updateBalance(Number(card.id),0),/expired/,`${dialect}: a zero adjustment on an expired card must be refused, not reported as success`);assert.equal(Number(await column("gift_cards",card.id,"current_balance")),100)});await check("payments recordRefund",async()=>{const payment=await storePayment({amount:1000,method:"creditCard",status:"completed",transaction_id:`TXN-WRITE-COUNTS-${dialect}`});assert(payment?.id,"seed payment");const partial=await recordRefund(Number(payment.id),250);assert.equal(Number(await column("payments",payment.id,"refund_amount")),250);assert.equal(partial.status,"partiallyRefunded");const full=await recordRefund(Number(payment.id),750);assert.equal(await column("payments",payment.id,"status"),"refunded");assert.equal(full.status,"refunded");await assert.rejects(recordRefund(Number(payment.id),1),/cannot be refunded/,"a fully refunded payment rejects another refund")});await check("product categories deactivate",async()=>{let next=0;const seedCategory=async(name,parentId)=>{next+=1;await db.insertInto("categories").values({uuid:crypto.randomUUID(),name,slug:`${name}-${dialect}-${next}`,display_order:next,is_active:!0,parent_category_id:parentId,created_at:sqlDateTime(),updated_at:sqlDateTime()}).execute();const rows=await db.selectFrom("categories").selectAll().execute();return Number(rows[rows.length-1].id)},id=await seedCategory("parent",null);assert.equal(await deactivate(id),!0,"deactivating a category that exists");assert.equal(Boolean(await column("categories",id,"is_active")),!1);assert.equal(await deactivate(id),!0,"deactivating a category that is already inactive");for(const child of["a","b"])await seedCategory(`child-${child}`,id);const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{assert.equal(await deactivateChildCategories(String(id)),2);assert.equal(await deactivateChildCategories(String(id)),2,`${dialect}: children that are already inactive are still the children deactivated`)}finally{setSystemTime()}const doomed=await seedCategory("doomed",null);let removed=!1;const unregister=registerPersistentQueryHooks({onQueryStart(event){if(event.kind!=="update"||removed)return;removed=deleteOnAnotherConnection(doomed)}});try{const claimed=await deactivate(doomed);assert.equal(removed,!0,"the competing delete must have run");assert.equal(claimed,!1,`${dialect}: deactivate must not report success for a row that is gone`)}finally{unregister()}assert.equal(await db.selectFrom("categories").selectAll().where("id","=",doomed).executeTakeFirst(),void 0)});await check("receipts bulkStore",async()=>{const receipt=(index)=>({printer:"Front Counter",document:`Receipt #${index}`,timestamp:Date.parse("2026-01-10T12:00:00Z"),status:"success",size:12,pages:1,duration:2}),before=await count("receipts"),reported=await bulkStore([receipt(1),receipt(2),receipt(3)]),inserted=await count("receipts")-before;assert.equal(inserted,3,"the three receipts were inserted");assert.equal(reported,inserted,`bulkStore reported ${reported} but inserted ${inserted} receipts`)});assert.deepEqual(failures,[],`${dialect}: every commerce write must apply and report the rows it changed`);console.log("commerce write counts OK")}finally{resetDatabaseConnection()}
8
+ `],{cwd:tmpdir(),env:process.env,stdout:"pipe",stderr:"pipe",timeout:5000});if(child.exitCode!==0)console.error(new TextDecoder().decode(child.stderr));return child.exitCode===0}const failures=[];async function check(name,run){try{await run();console.log(`PASS ${name}`)}catch(error){failures.push(`${name}: ${error instanceof Error?error.message:String(error)}`)}}try{for(const statement of statements)await db.unsafe(statement).execute();const today=new Date;await check("orders cleanupAbandonedCarts",async()=>{const old="2020-01-01 00:00:00",fresh=today.toISOString().slice(0,19).replace("T"," ");for(const[index,updatedAt]of[old,old,fresh].entries())await db.insertInto("carts").values({uuid:`cart-${index}`,applied_coupon_id:"",expires_at:updatedAt,created_at:updatedAt,updated_at:updatedAt}).execute();const before=await count("carts"),{deleted}=await cleanupAbandonedCarts({olderThanDays:30}),removed=before-await count("carts");assert.equal(removed,2,"the two abandoned carts were deleted");assert.equal(deleted,removed,`cleanupAbandonedCarts reported ${deleted} but removed ${removed} carts`)});await check("coupons redeem",async()=>{const today=new Date,day=(offset)=>new Date(today.getTime()+offset*86400000).toISOString().slice(0,10),coupon=await storeCoupon({code:`REDEEM-${dialect}`,discount_type:"percentage",discount_value:10,product_id:1,start_date:day(-1),end_date:day(30),is_active:!0,usage_limit:2});assert(coupon?.id,"seed coupon");const id=Number(coupon.id),first=await redeem(id);assert.equal(Number(await column("coupons",id,"usage_count")),1,"the redemption was committed");assert.equal(first.ok,!0,`redeem reported ${JSON.stringify(first)} for a redemption it committed`);assert.equal((await redeem(id)).ok,!0);const third=await redeem(id);assert.equal(third.ok,!1);assert.equal(third.ok===!1&&third.reason,"limit-reached");assert.equal(Number(await column("coupons",id,"usage_count")),2,"a rejected redemption must not count")});await check("gift cards updateBalance",async()=>{const card=await storeGiftCard({code:`GC-WRITE-COUNTS-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0});assert(card?.id,"seed gift card");const spent=await updateBalance(Number(card.id),-40);assert.equal(Number(await column("gift_cards",card.id,"current_balance")),60);assert.equal(Number(spent?.current_balance),60);const used=await updateBalance(Number(card.id),-60);assert.equal(await column("gift_cards",card.id,"status"),"USED");assert.equal(used?.status,"USED");await assert.rejects(updateBalance(Number(card.id),-1),/not active/,"a spent card rejects another redemption")});await check("gift cards updateBalance by zero",async()=>{const card=await storeGiftCard({code:`GC-ZERO-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0});assert(card?.id,"seed gift card");const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{await updateBalance(Number(card.id),0);const again=await updateBalance(Number(card.id),0);assert.equal(Number(again?.current_balance),100,`${dialect}: a zero adjustment must return the card unchanged`);await assert.rejects(updateBalance(Number(card.id),-1000),/Insufficient/,"an over-spend is still insufficient");assert.equal(Number(await column("gift_cards",card.id,"current_balance")),100)}finally{setSystemTime()}});await check("gift cards updateBalance on an expired card",async()=>{const card=await storeGiftCard({code:`GC-EXPIRED-${dialect}`,initial_balance:100,current_balance:100,currency:"USD",status:"ACTIVE",is_active:!0,expiry_date:"2020-01-01 00:00:00"});assert(card?.id,"seed gift card");await assert.rejects(updateBalance(Number(card.id),-10),/expired/,`${dialect}: a redemption on an expired card must say it expired`);await assert.rejects(updateBalance(Number(card.id),0),/expired/,`${dialect}: a zero adjustment on an expired card must be refused, not reported as success`);assert.equal(Number(await column("gift_cards",card.id,"current_balance")),100)});await check("gift card and license key dates sent as Unix seconds",async()=>{const stored=async(table,id,name)=>parseSqlDateTime(await column(table,id,name))?.toISOString(),card=await storeGiftCard({code:`GC-DATES-${dialect}`,initial_balance:50,current_balance:50,currency:"USD",status:"ACTIVE",is_active:!0,expiry_date:1798761600});assert(card?.id,"gift card with a seconds expiry");assert.equal(await stored("gift_cards",card.id,"expiry_date"),"2027-01-01T00:00:00.000Z");await updateGiftCard(Number(card.id),{expiry_date:1830297600});assert.equal(await stored("gift_cards",card.id,"expiry_date"),"2028-01-01T00:00:00.000Z");const key=await storeLicenseKey({key:"ABCD-EFGH-IJKL-MNOP-QRST",template:"Standard License",status:"unassigned",expiry_date:1798761600});assert(key?.id,"license key with a seconds expiry");assert.equal(await stored("license_keys",key.id,"expiry_date"),"2027-01-01T00:00:00.000Z");await updateLicenseKey(Number(key.id),{status:"active"});assert.equal(await stored("license_keys",key.id,"expiry_date"),"2027-01-01T00:00:00.000Z")});await check("payments recordRefund",async()=>{const payment=await storePayment({amount:1000,method:"creditCard",status:"completed",transaction_id:`TXN-WRITE-COUNTS-${dialect}`});assert(payment?.id,"seed payment");const partial=await recordRefund(Number(payment.id),250);assert.equal(Number(await column("payments",payment.id,"refund_amount")),250);assert.equal(partial.status,"partiallyRefunded");const full=await recordRefund(Number(payment.id),750);assert.equal(await column("payments",payment.id,"status"),"refunded");assert.equal(full.status,"refunded");await assert.rejects(recordRefund(Number(payment.id),1),/cannot be refunded/,"a fully refunded payment rejects another refund")});await check("product categories deactivate",async()=>{let next=0;const seedCategory=async(name,parentId)=>{next+=1;await db.insertInto("categories").values({uuid:crypto.randomUUID(),name,slug:`${name}-${dialect}-${next}`,display_order:next,is_active:!0,parent_category_id:parentId,created_at:sqlDateTime(),updated_at:sqlDateTime()}).execute();const rows=await db.selectFrom("categories").selectAll().execute();return Number(rows[rows.length-1].id)},id=await seedCategory("parent",null);assert.equal(await deactivate(id),!0,"deactivating a category that exists");assert.equal(Boolean(await column("categories",id,"is_active")),!1);assert.equal(await deactivate(id),!0,"deactivating a category that is already inactive");for(const child of["a","b"])await seedCategory(`child-${child}`,id);const{setSystemTime}=await import("bun:test");setSystemTime(new Date("2030-01-02T03:04:05.000Z"));try{assert.equal(await deactivateChildCategories(String(id)),2);assert.equal(await deactivateChildCategories(String(id)),2,`${dialect}: children that are already inactive are still the children deactivated`)}finally{setSystemTime()}const doomed=await seedCategory("doomed",null);let removed=!1;const unregister=registerPersistentQueryHooks({onQueryStart(event){if(event.kind!=="update"||removed)return;removed=deleteOnAnotherConnection(doomed)}});try{const claimed=await deactivate(doomed);assert.equal(removed,!0,"the competing delete must have run");assert.equal(claimed,!1,`${dialect}: deactivate must not report success for a row that is gone`)}finally{unregister()}assert.equal(await db.selectFrom("categories").selectAll().where("id","=",doomed).executeTakeFirst(),void 0)});await check("category stats name the parent with the most children",async()=>{const insert=async(name,parentId)=>{await db.insertInto("categories").values({uuid:crypto.randomUUID(),name,slug:`${name}-${dialect}`,display_order:1,is_active:!0,parent_category_id:parentId,created_at:sqlDateTime(),updated_at:sqlDateTime()}).execute();const row=await db.selectFrom("categories").selectAll().where("name","=",name).executeTakeFirst();return Number(row.id)},parent=await insert("stats-parent",null);for(const child of["x","y","z"])await insert(`stats-child-${child}`,parent);const stats=await fetchCategoryStats();assert.deepEqual(stats.top_parent_categories[0],{id:String(parent),name:"stats-parent",child_count:3})});await check("receipts bulkStore",async()=>{const receipt=(index)=>({printer:"Front Counter",document:`Receipt #${index}`,timestamp:Date.parse("2026-01-10T12:00:00Z"),status:"success",size:12,pages:1,duration:2}),before=await count("receipts"),reported=await bulkStore([receipt(1),receipt(2),receipt(3)]),inserted=await count("receipts")-before;assert.equal(inserted,3,"the three receipts were inserted");assert.equal(reported,inserted,`bulkStore reported ${reported} but inserted ${inserted} receipts`)});assert.deepEqual(failures,[],`${dialect}: every commerce write must apply and report the rows it changed`);console.log("commerce write counts OK")}finally{resetDatabaseConnection()}
@@ -1 +1 @@
1
- export function insertedId(result){if(typeof result==="bigint")return normalize(result);if(typeof result==="number")return normalize(result);if(!result||typeof result!=="object")return;if(Array.isArray(result)){for(const item of result){const found=insertedId(item);if(found!==void 0)return found}return}const record=result;for(const key of["lastInsertRowid","insertId","lastInsertId"]){const value=record[key];if(value===void 0||value===null)continue;const id=normalize(value);if(id!==void 0)return id}return}function normalize(value){const id=typeof value==="bigint"?Number(value):Number(value);if(!Number.isSafeInteger(id)||id<=0)return;return id}
1
+ export function insertedId(result){if(typeof result==="bigint")return normalize(result);if(typeof result==="number")return normalize(result);if(!result||typeof result!=="object")return;if(Array.isArray(result)){for(const item of result){const found=insertedId(item);if(found!==void 0)return found}return}const record=result;for(const key of["id","lastInsertRowid","insertId","lastInsertId"]){const value=record[key];if(value===void 0||value===null)continue;const id=normalize(value);if(id!==void 0)return id}return}function normalize(value){const id=typeof value==="bigint"?Number(value):Number(value);if(!Number.isSafeInteger(id)||id<=0)return;return id}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/commerce",
3
3
  "type": "module",
4
- "version": "0.75.318",
4
+ "version": "0.75.320",
5
5
  "description": "Stacks commerce utilities.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -57,18 +57,19 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@stacksjs/cache": "0.75.318",
61
- "@stacksjs/config": "0.75.318",
62
- "@stacksjs/database": "0.75.318",
63
- "@stacksjs/env": "0.75.318",
64
- "@stacksjs/error-handling": "0.75.318",
65
- "@stacksjs/events": "0.75.318",
66
- "@stacksjs/logging": "0.75.318",
67
- "@stacksjs/orm": "0.75.318",
68
- "@stacksjs/payments": "0.75.318",
69
- "@stacksjs/realtime": "0.75.318",
70
- "@stacksjs/strings": "0.75.318",
71
- "@stacksjs/validation": "0.75.318",
60
+ "@stacksjs/cache": "0.75.320",
61
+ "@stacksjs/config": "0.75.320",
62
+ "@stacksjs/database": "0.75.320",
63
+ "@stacksjs/env": "0.75.320",
64
+ "@stacksjs/error-handling": "0.75.320",
65
+ "@stacksjs/events": "0.75.320",
66
+ "@stacksjs/logging": "0.75.320",
67
+ "@stacksjs/orm": "0.75.320",
68
+ "@stacksjs/payments": "0.75.320",
69
+ "@stacksjs/realtime": "0.75.320",
70
+ "@stacksjs/slug": "0.75.320",
71
+ "@stacksjs/strings": "0.75.320",
72
+ "@stacksjs/validation": "0.75.320",
72
73
  "ts-spreadsheets": "^0.2.0"
73
74
  },
74
75
  "devDependencies": {