{"_id":"@by-association-only/metaobjects","_rev":"8-0dc5ecec8bec0124bdf6c003765efd7d","name":"@by-association-only/metaobjects","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@by-association-only/metaobjects","version":"0.1.0","_id":"@by-association-only/metaobjects@0.1.0","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"4d6e318a182085b3a33017cb28de4d75e771cd06","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.1.0.tgz","fileCount":6,"integrity":"sha512-pvXNPmUhf8LM2/j8vRRSER7LrDccABFMHp2KnFQY9TmTACIsc8eke3e34BCyqjZCfpfhP/nKux5bhOGakBsv7w==","signatures":[{"sig":"MEYCIQDUugV+GBal4N/4ox5U4QmjSwkXGYxz1SU9vxrLqCqUHwIhAIFUJnnWDYSMEPY1XAgmIeIPppl2bitHw6hWMDM/ycbD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136710},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"4d6e318a182085b3a33017cb28de4d75e771cd06","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-pvXNPmUhf8LM2/j8vRRSER7LrDccABFMHp2KnFQY9TmTACIsc8eke3e34BCyqjZCfpfhP/nKux5bhOGakBsv7w==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.1.0","@by-association-only/shopify-graphql-client":"1.2.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.1.0_1783344200701_0.9681974346852873","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@by-association-only/metaobjects","version":"0.1.1","_id":"@by-association-only/metaobjects@0.1.1","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"e412d7e219d7e590a69563e34aee257684181de5","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.1.1.tgz","fileCount":6,"integrity":"sha512-XQsylhhb/Ra7qT+wwmpTMV6TGm77e8/P5FQF2DLywwlRIhi/dY3K6SWspQELeFmx28pg2J/VY+pYC8DxqYgKeA==","signatures":[{"sig":"MEUCIQDHSYaNOVbU3l7g734QVtzC3wNTaI6SdE/SL3ZsVIwV8gIgR5hQ/rsxG7lTNe+6EJK1xWCTuPgNUK96O6dHgXGfjcM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137378},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"e412d7e219d7e590a69563e34aee257684181de5","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-XQsylhhb/Ra7qT+wwmpTMV6TGm77e8/P5FQF2DLywwlRIhi/dY3K6SWspQELeFmx28pg2J/VY+pYC8DxqYgKeA==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.2.0","@by-association-only/shopify-graphql-client":"1.3.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^7.0.1-rc"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.1.1_1783355090829_0.01293321568071848","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@by-association-only/metaobjects","version":"0.1.2","_id":"@by-association-only/metaobjects@0.1.2","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"001c77a83cb3f4e79aac4ff4cf33458375de0b9c","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.1.2.tgz","fileCount":6,"integrity":"sha512-ZzaKMcipgdOk2QkO4+18kx3KG3l3OvF1kVouA1nY6sbMb+aDpxKjrdFZ9relFQ1Uz6OFesVs8mrwRntLGAMArQ==","signatures":[{"sig":"MEUCIEcjxH/5qD0S5aj/a3LQNDJsyjgtNFr20Z2g6M3cddXtAiEA8bDJvO7fGKVQ6O/cmNrHlVhnShatkUoCR1NaX6LJLkg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137378},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"001c77a83cb3f4e79aac4ff4cf33458375de0b9c","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-ZzaKMcipgdOk2QkO4+18kx3KG3l3OvF1kVouA1nY6sbMb+aDpxKjrdFZ9relFQ1Uz6OFesVs8mrwRntLGAMArQ==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.2.0","@by-association-only/shopify-graphql-client":"1.3.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^7.0.1-rc"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.1.2_1783359862649_0.7120236898319832","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@by-association-only/metaobjects","version":"0.2.0","_id":"@by-association-only/metaobjects@0.2.0","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"2a9780e28d46ab68fa46d0789c04cdd3f09ab0ab","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.2.0.tgz","fileCount":7,"integrity":"sha512-oS5/RQXm0oyi5Dh2WmaYMXz3SdgP1J343Pl3aMt4h2sVJUvjZhzhrVZIjWU6ITvskhyTvdtLqZXIUs58hivJqA==","signatures":[{"sig":"MEUCIQCODhUJF0tEzisUYNk5TztCTa5u6Rx8J3jlDnXvnx0gfQIgVuQZ4osC9wqj7WISJo91Ap7CmqeYKh4XCg+AYSk/sXU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":159046},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"2a9780e28d46ab68fa46d0789c04cdd3f09ab0ab","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-oS5/RQXm0oyi5Dh2WmaYMXz3SdgP1J343Pl3aMt4h2sVJUvjZhzhrVZIjWU6ITvskhyTvdtLqZXIUs58hivJqA==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.3.0","@by-association-only/shopify-graphql-client":"1.3.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","typescript":"^7.0.1-rc"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.2.0_1783445458802_0.5539688508649716","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@by-association-only/metaobjects","version":"0.2.1","_id":"@by-association-only/metaobjects@0.2.1","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"c76da498c27c188d1a1b29cbb40b9a2359e5cbe4","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.2.1.tgz","fileCount":7,"integrity":"sha512-KA0+iZ1+TpFuFlGuJEeTxUMOGuXGyh7UchDvTi6hDjq9GtbOwLQfmr3C41NForWqvr/DB5YNNS/Z/RPHtqha0w==","signatures":[{"sig":"MEQCIAXnWR4kzMgizIfc4r16HNiifBS9GOmJuA6TSMfM7ZLeAiAbRoCzUJypktK/TUwfYp0307GX7QWGVPB5BnDxnq6fDw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":159046},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"c76da498c27c188d1a1b29cbb40b9a2359e5cbe4","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-KA0+iZ1+TpFuFlGuJEeTxUMOGuXGyh7UchDvTi6hDjq9GtbOwLQfmr3C41NForWqvr/DB5YNNS/Z/RPHtqha0w==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.3.0","@by-association-only/shopify-graphql-client":"1.3.3"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","typescript":"^7.0.1-rc"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.2.1_1783463060204_0.9279243865988729","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@by-association-only/metaobjects","version":"0.2.2","_id":"@by-association-only/metaobjects@0.2.2","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"ffbcf7874c4e557c3a235ee7c6b68c9cb198fc79","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.2.2.tgz","fileCount":7,"integrity":"sha512-LFlNH3bk3NPcQViyRHVD9UbTCPebJXMR2LNqi/PFpLdr5F53gf/P0YEUjp5701hiy84ScQGSmfp+Wg9eB4vftw==","signatures":[{"sig":"MEMCIEiSvY1rB3lOZDDEEzl0TSYs/pR2W4D3fx1QI88aHBErAh9srmPihAPA/jJsn/laCzYR0kASpCLnOq0clRRr872c","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":159043},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"ffbcf7874c4e557c3a235ee7c6b68c9cb198fc79","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-LFlNH3bk3NPcQViyRHVD9UbTCPebJXMR2LNqi/PFpLdr5F53gf/P0YEUjp5701hiy84ScQGSmfp+Wg9eB4vftw==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"24.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.3.0","@by-association-only/shopify-graphql-client":"1.4.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","typescript":"^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.2.2_1784117920493_0.7472829950350641","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@by-association-only/metaobjects","version":"0.3.0","_id":"@by-association-only/metaobjects@0.3.0","maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"dist":{"shasum":"fc46e2f81674b0fe0abaa1c361f9161ccc7b3158","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.3.0.tgz","fileCount":7,"integrity":"sha512-oTRdVSXj/aguutZxby4WA5exYD2tDGdYGqWvwse/Pmq9wyM1tt4Bl8+wm/nB9hJasQMjBZZmRy/CMMBx4jm1Qw==","signatures":[{"sig":"MEUCIDHkJSzA+l9UYRitTasl0HJul/WDy9msPrr6knwcWBgHAiEAk02/soP+c/JokbKSEZyvwfrl7jqJBHt2bLicIbhORI8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":167245},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"fc46e2f81674b0fe0abaa1c361f9161ccc7b3158","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-oTRdVSXj/aguutZxby4WA5exYD2tDGdYGqWvwse/Pmq9wyM1tt4Bl8+wm/nB9hJasQMjBZZmRy/CMMBx4jm1Qw==","_npmVersion":"10.8.3","directories":{},"_nodeVersion":"26.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.3.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","typescript":"^7.0.2","@by-association-only/shopify-graphql-client":"1.4.0"},"peerDependencies":{"@by-association-only/shopify-graphql-client":"^1.4.0"},"_npmOperationalInternal":{"tmp":"tmp/metaobjects_0.3.0_1788271865492_0.3761485248601364","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"@by-association-only/metaobjects@0.4.0","dist":{"shasum":"55a320771c782c56af809c6dbe02dc71fa71d124","tarball":"https://registry.npmjs.org/@by-association-only/metaobjects/-/metaobjects-0.4.0.tgz","fileCount":7,"integrity":"sha512-X2Lca5IBcJkhPgk+kgzUpKwjgpvCWf5FWVUTmbN8nnFw37ItIKQIFUT81SI6wxroqhmqlDQ4LeHX6uIQlkrHwQ==","signatures":[{"sig":"MEUCICI7TzbZuxHHv6xzF7JNUye/hNoWbWtQsTpMs8IXHX5vAiEA+H5lMfzRgcdQdz7ObwRuxd6jeGyQyId54qoQyBNsBD0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDC8t+O4S5iTrCleNKKsSangDykNB1twmgvIO9k0RXs8QIgYSUMtEoeKKF6ALtjp5YIL6+8GbbhFGzC0LTjCapeBys="}],"unpackedSize":168044},"main":"./dist/index.cjs","name":"@by-association-only/metaobjects","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"55a320771c782c56af809c6dbe02dc71fa71d124","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit"},"version":"0.4.0","_npmUser":{"name":"dangamble","email":"dan@bao.agency"},"_integrity":"sha512-X2Lca5IBcJkhPgk+kgzUpKwjgpvCWf5FWVUTmbN8nnFw37ItIKQIFUT81SI6wxroqhmqlDQ4LeHX6uIQlkrHwQ==","_npmVersion":"10.8.3","directories":{},"maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"_nodeVersion":"26.3.0","dependencies":{"zod":"^4.1.12","@shopify/admin-graphql-api-utilities":"^2.2.0","@by-association-only/shopify-admin-types":"0.3.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.27.3","typescript":"^7.0.2","@by-association-only/shopify-graphql-client":"1.4.0"},"peerDependencies":{"@by-association-only/shopify-graphql-client":"^1.4.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/metaobjects_0.4.0_1789980466227_0.06352570094726273"}}},"time":{"created":"2026-07-06T13:23:20.504Z","modified":"2026-09-21T08:47:46.493Z","0.1.0":"2026-07-06T13:23:20.867Z","0.1.1":"2026-07-06T16:24:50.974Z","0.1.2":"2026-07-06T17:44:22.819Z","0.2.0":"2026-07-07T17:30:58.941Z","0.2.1":"2026-07-07T22:24:20.330Z","0.2.2":"2026-07-15T12:18:40.637Z","0.3.0":"2026-09-01T14:11:05.617Z","0.4.0":"2026-09-21T08:47:46.317Z"},"maintainers":[{"name":"dangamble","email":"dan@bao.agency"},{"name":"eng9911","email":"m.p.england@gmail.com"}],"readme":"# @by-association-only/metaobjects\n\nDefine a Shopify metaobject type once with a field schema and get back a typed\nclass with CRUD, querying, and automatic serialisation to and from Shopify's\n`{ key, value }[]` field format. Every app that touches metaobjects otherwise\nhand-rolls the same serialisation and boilerplate: turning numbers and booleans\ninto strings, JSON-encoding structured values, mapping snake_case Shopify keys\nback to camelCase properties, and wiring up `metaobjectCreate` /\n`metaobjectUpdate` / `metaobjectDelete`. This package does it from the schema, so\nyou write the schema and get the rest.\n\n## Install\n\n```bash\nbun add @by-association-only/metaobjects\n```\n\nEverything runtime takes an `AdminApiClient` and makes its own GraphQL calls, so\nyou also need the client that provides it:\n\n```bash\nbun add @by-association-only/shopify-graphql-client\n```\n\nThe package is transport-agnostic. It has no coupling to TanStack Router, Query,\nor any framework: pass a client, get results back.\n\n## Quick start\n\n```ts\nimport {\n  defineMetaobject,\n  MetafieldType,\n} from \"@by-association-only/metaobjects\";\n\nconst Store = defineMetaobject(\"$app:store\", {\n  title: MetafieldType.SingleLineTextField,\n  address1: { key: \"address_1\", type: MetafieldType.SingleLineTextField },\n  sortOrder: { key: \"sort_order\", type: MetafieldType.NumberInteger },\n  notes: { type: MetafieldType.MultiLineTextField, optional: true },\n});\n```\n\nThat call returns a class. Use its statics for the ID-plus-values operations\n(list, load, create, form-style update, handle-keyed upsert, bulk delete) and\nthe instance `update` when you already have a loaded record:\n\n```ts\n// List. Defaults to the first 50, forward paginated.\nconst page = await Store.all(client);\npage.nodes;      // Store[]\npage.pageInfo;   // { hasNextPage, hasPreviousPage, startCursor, endCursor }\n\n// Load one by GID or numeric id (a number is composed into a GID for you).\nconst store = await Store.find(client, \"gid://shopify/Metaobject/123\");\nif (store) {\n  store.fields.title;     // string\n  store.fields.sortOrder; // number\n  store.id;               // ShopifyGid<\"Metaobject\">\n  store.handle;\n  store.createdAt;        // Date\n}\n\n// Create. Returns a DomainResult, not the record directly.\nconst created = await Store.create(client, {\n  title: \"London\",\n  address1: \"1 High Street\",\n  sortOrder: 3,\n});\nif (created.ok) {\n  created.data.fields.title; // \"London\"\n}\n\n// Update, form-style: id + values (e.g. a form submission).\nawait Store.update(client, store.id, { title: \"New name\" });\n\n// Update, instance-style: you already loaded the record.\nawait store.update(client, { title: \"New name\" });\n\n// Upsert by handle: one idempotent write, exists or not.\nawait Store.upsert(client, \"store-london\", {\n  title: \"London\",\n  address1: \"1 High Street\",\n  sortOrder: 3,\n});\n\n// Delete one or many.\nawait Store.deleteMany(client, [store.id]);\n```\n\n`fields` access is typed off the schema, so `store.fields.sortOrder` is a\n`number` and `store.fields.notes` is `string | null` (optional).\n\n### Method reference\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `Store.all` | `(client, params?)` | `MetaobjectPage<Store>` |\n| `Store.find` | `(client, id: string \\| number)` | `Store \\| null` |\n| `Store.create` | `(client, fields, handle?, status?)` | `DomainResult<Store>` |\n| `Store.update` | `(client, id, values, status?)` | `DomainResult<Store>` |\n| `store.update` | `(client, values)` | `DomainResult<Store>` |\n| `Store.upsert` | `(client, handle, fields, status?)` | `DomainResult<Store>` |\n| `Store.deleteMany` | `(client, ids: string[])` | `DeleteManyResult` |\n| `Store.hydrate` | `(node)` | `Store` |\n| `Store.type` | static property | `string` |\n| `Store.schema` | static property | the schema you passed |\n\n`deleteMany` is the odd one out: it does not return a `DomainResult`, it returns\na per-id breakdown so a partial failure tells you which ids failed. See\n[Results and errors](#results-and-errors).\n\n`all` accepts standard connection params, validated by\n`metaobjectQueryParamsSchema` (a Zod schema exported for reuse):\n\n```ts\nawait Store.all(client, {\n  first: 20,\n  query: \"title:London\",\n  sortKey: \"updated_at\",\n  reverse: true,\n});\n\n// Backward pagination: passing `before` or `last` flips to a backward page,\n// defaulting `last` to 50.\nawait Store.all(client, { before: cursor });\n```\n\n## Field schemas\n\nA schema maps a property name to a field definition. Two forms:\n\n```ts\n{\n  // String shorthand: use when the property name IS the Shopify field key.\n  title: MetafieldType.SingleLineTextField,\n\n  // Object form: use when they differ, i.e. camelCase property to snake_case key.\n  sortOrder: { key: \"sort_order\", type: MetafieldType.NumberInteger },\n\n  // Optional. Adds `?` and widens the value to `T | null`.\n  notes: { type: MetafieldType.MultiLineTextField, optional: true },\n\n  // List. The value type becomes an array.\n  items: { type: MetafieldType.List.MetaobjectReference, optional: true },\n}\n```\n\n`optional` must be the literal `true`. It is what splits required keys\n(`T`) from optional keys (`T | null` with `?`) in the inferred value type.\n\n`MetafieldType` covers the Shopify metafield types and maps each to a TypeScript\ntype you actually work with. A selection:\n\n| `MetafieldType.*` | TypeScript type |\n| --- | --- |\n| `SingleLineTextField`, `MultiLineTextField` | `string` |\n| `NumberInteger`, `NumberDecimal` | `number` |\n| `Boolean` | `boolean` |\n| `Date`, `DateTime` | `string` |\n| `Color` | `` `#${string}` `` |\n| `Url`, `Id` | `string` |\n| `Json` | `unknown` |\n| `Money` | `{ amount: string; currency_code: string }` |\n| `Weight` | `{ value: number; unit: WeightUnit }` |\n| `Volume`, `Dimension` | `{ value: number; unit: ... }` |\n| `Link` | `{ text: string; url: string }` |\n| `Rating` | `{ value: string; scale_min: string; scale_max: string }` |\n| `RichTextField` | `RichTextRoot` |\n| `ProductReference` | `ShopifyGid<\"Product\">` |\n| `VariantReference` | `ShopifyGid<\"ProductVariant\">` |\n| `MetaobjectReference` | `ShopifyGid<\"Metaobject\">` |\n| `CustomerReference` | `ShopifyGid<\"Customer\">` |\n\nReference types for articles, collections, companies, files, pages, and so on\nfollow the same pattern. Every scalar type has a `List.*` counterpart\n(`MetafieldType.List.NumberInteger`, `MetafieldType.List.MetaobjectReference`,\nand so on) whose value type is the array of the scalar.\n\nTo pull the inferred value type out of a schema, use `InferFieldValues`:\n\n```ts\nimport {\n  type InferFieldValues,\n  type MetaobjectSchema,\n  MetafieldType,\n} from \"@by-association-only/metaobjects\";\n\nconst registrySchema = {\n  title: MetafieldType.SingleLineTextField,\n  eventDate: { type: MetafieldType.Date, key: \"event_date\" },\n  shippingAddress: { type: MetafieldType.Json, key: \"shipping_address\" },\n  archived: MetafieldType.Boolean,\n} as const satisfies MetaobjectSchema;\n\ntype RegistryFields = InferFieldValues<typeof registrySchema>;\n// { title: string; eventDate: string; shippingAddress: unknown; archived: boolean }\n```\n\nThe `as const satisfies MetaobjectSchema` is the idiom: `as const` keeps the\nliteral type strings so inference works, `satisfies` checks the shape without\nwidening it.\n\n## Serialisation\n\nShopify stores every field as a string. `serializeFields` and\n`deserializeFields` convert between typed values and that string format, and the\nCRUD methods call them for you. The rules:\n\n- **Numbers and booleans** become their string form on write (`3` to `\"3\"`,\n  `true` to `\"true\"`) and are parsed back on read. Integers are rounded on read.\n- **Structured types** (`money`, `link`, `rating`, `weight`, `volume`,\n  `dimension`, `rich_text_field`, `json`) are JSON-encoded on write and\n  `JSON.parse`d on read.\n- **Lists** are stored as a JSON array of the serialised scalar values.\n- **`undefined` is dropped on serialise; `null` clears.** An `undefined` value\n  never reaches Shopify, so a partial update only writes the keys you set. A\n  `null` value is sent as `\"\"`, which Shopify accepts as a clear for text and\n  reference fields alike.\n- **An empty string deserialises to `null`.** A blank Shopify field comes back as\n  `null`, not `\"\"`.\n\nYou rarely call these directly, but they are exported if you need to serialise\noutside a CRUD path:\n\n```ts\nimport { serializeFields, deserializeFields } from \"@by-association-only/metaobjects\";\n\nserializeFields(registrySchema, { title: \"Wedding\", archived: false });\n// [{ key: \"title\", value: \"Wedding\" }, { key: \"archived\", value: \"false\" }]\n```\n\n## Subclassing\n\nThe returned class is a real class, so extend it to add domain behaviour. The\nstatics stay typed to the subclass: `Registry.find(...)` returns `Registry` with\nyour methods, not the base instance. This is deliberate. The runtime uses\n`new this` late-binding so a subclass hydrates into its own type, and the static\nsignatures mirror that.\n\nModelled on abask's registry domain:\n\n```ts\nimport {\n  attachToCustomer,\n  customerListConfig,\n  defineMetaobject,\n  type InferFieldValues,\n  MetafieldType,\n  type MetaobjectSchema,\n} from \"@by-association-only/metaobjects\";\nimport type { AdminApiClient } from \"@by-association-only/shopify-graphql-client\";\nimport type { ShopifyGid } from \"@shopify/admin-graphql-api-utilities\";\n\nexport const registryType = \"$app:registry\" as const;\n\nconst registrySchema = {\n  title: MetafieldType.SingleLineTextField,\n  eventDate: { type: MetafieldType.Date, key: \"event_date\" },\n  shippingAddress: { type: MetafieldType.Json, key: \"shipping_address\" },\n  items: { type: MetafieldType.List.MetaobjectReference, optional: true },\n} as const satisfies MetaobjectSchema;\n\ntype RegistryShippingAddress = {\n  line1: string;\n  city: string;\n  country: string;\n  postcode: string;\n};\n\nconst customerRegistriesConfig = customerListConfig(\"registries\");\n\nexport class Registry extends defineMetaobject(registryType, registrySchema) {\n  /** `shipping_address` is a json field; this is its one honest shape. */\n  get shippingAddress(): RegistryShippingAddress {\n    return this.fields.shippingAddress as RegistryShippingAddress;\n  }\n\n  async attachToCustomer(\n    client: AdminApiClient,\n    customerId: ShopifyGid<\"Customer\">,\n  ): Promise<DomainVoidResult> {\n    return attachToCustomer(client, this.id, customerId, customerRegistriesConfig);\n  }\n}\n```\n\n`Registry.find(client, id)` now returns a `Registry`, so `.shippingAddress` and\n`.attachToCustomer` are there. So does `Registry.create(...).data`,\n`Registry.all(...).nodes`, and `instance.update(...).data`.\n\nYou can also override a static to add a guard. A subclass override calls\n`super.update` but has to re-cast the result, because `super.update` types\nagainst the base class rather than the subclass `T`:\n\n```ts\nstatic override async update<T extends typeof RegistryItemBase>(\n  this: T,\n  client: AdminApiClient,\n  id: string | number,\n  values: Partial<RegistryItemFields>,\n  status?: MetaobjectStatus,\n): Promise<DomainResult<InstanceType<T>>> {\n  // ...validate, then:\n  return super.update(client, id, values, status) as Promise<\n    DomainResult<InstanceType<T>>\n  >;\n}\n```\n\n## The registry\n\nEvery class from `defineMetaobject` self-registers under its `type` string in a\nprocess-global map. You can look a class up by type without importing it\ndirectly:\n\n```ts\nimport {\n  getMetaobjectClass,\n  listRegisteredMetaobjectTypes,\n} from \"@by-association-only/metaobjects\";\n\ngetMetaobjectClass(\"$app:store\");     // the Store class, or undefined\nlistRegisteredMetaobjectTypes();      // [\"$app:store\", \"$app:registry\", ...]\n```\n\nThis is what lets a generic layer hydrate a node when it only knows the type\nstring at runtime, for instance a webhook handler or a serialisation boundary\nthat reconstructs instances from wire data. `registerMetaobject(type, ctor)` and\nthe `BaseMetaobjectInstance` base class are exported for that plumbing.\n\n## Customer-list joins\n\nA metaobject is often joined to a customer through a\n`list.metaobject_reference` metafield: a customer has many registries or many\nwishlists, held as a list of metaobject GIDs on the customer. The customer-list\nhelpers read and write that list.\n\n`customerListConfig(key)` builds a config pinned to the customer's `$app`\nmetafield at `key`. The attach and detach helpers take that config:\n\n```ts\nimport {\n  attachToCustomer,\n  customerListConfig,\n  detachFromCustomer,\n  detachManyFromCustomer,\n} from \"@by-association-only/metaobjects\";\n\nconst config = customerListConfig(\"registries\");\n\n// Append. Idempotent: no duplicate if the id is already in the list.\nawait attachToCustomer(client, registryId, customerId, config);\n\n// Remove. Idempotent: a no-op if the id is absent.\nawait detachFromCustomer(client, registryId, customerId, config);\n\n// Bulk remove: one read and one write for many ids belonging to one customer.\nawait detachManyFromCustomer(client, [id1, id2], customerId, config);\n```\n\nEach returns a `DomainVoidResult`. The realistic shape is to wrap `attach` in a\ndomain method on the subclass, as `Registry.attachToCustomer` above does, so the\nconfig and key live in one place.\n\n## reconcileChildren\n\n`reconcileChildren` syncs a desired list of child metaobjects to Shopify. It is\ngeneric over any parent/child shape (steps to options, registry to items,\nsections to blocks), so you supply the operations. It splits the desired list\nby id and runs three phases:\n\n1. **Create** every item whose id is not a metaobject GID (a temporary id such\n   as `temp_1` marks a new child).\n2. **Update** every item whose id is a real GID.\n3. **Delete** every GID in `currentIds` that is absent from the desired list.\n\nIt returns the final ordered GID list, matching the order of `desired`.\n\n**There is no rollback.** If a phase fails it stops and returns the errors;\nanything already created or updated in an earlier phase stays put. Fix the cause\nand reconcile again. Because create and update are idempotent against the same\ndesired state, re-running converges.\n\nYou pass `ops` describing how to create, update, delete, and read ids for your\nchild type:\n\n```ts\nimport {\n  reconcileChildren,\n  type ReconcileOps,\n} from \"@by-association-only/metaobjects\";\nimport type { ShopifyGid } from \"@shopify/admin-graphql-api-utilities\";\n\ntype Child = { id: string; label: string };\n\nconst ops: ReconcileOps<Child, { id: ShopifyGid<\"Metaobject\"> }> = {\n  create: async (client, item) => {\n    const result = await ChildMetaobject.create(client, { label: item.label });\n    if (!result.ok) throw new Error(result.errors[0]?.message);\n    return { id: result.data.id };\n  },\n  update: async (client, id, item) => {\n    const result = await ChildMetaobject.update(client, id, { label: item.label });\n    if (!result.ok) throw new Error(result.errors[0]?.message);\n    return { id: result.data.id };\n  },\n  delete: async (client, id) => {\n    await ChildMetaobject.deleteMany(client, [id]);\n  },\n  getChildId: (item) => item.id,\n  getId: (result) => result.id,\n};\n\n// current holds the parent's existing child GIDs; desired is the new list,\n// with temp ids for new children and real GIDs for the ones that survive.\nconst result = await reconcileChildren(\n  client,\n  currentChildIds,\n  [\n    { id: existingChildGid, label: \"kept and renamed\" }, // updated\n    { id: \"temp_new\", label: \"brand new\" },              // created\n    // any GID in currentChildIds not listed here is deleted\n  ],\n  ops,\n);\n\nif (result.ok) {\n  result.data.ids; // ordered GIDs, matching the desired order\n  // write these back onto the parent's list.metaobject_reference field\n}\n```\n\n`ops.create` and `ops.update` throw on failure. `reconcileChildren` catches the\nthrow, stops the run, and returns the message as a `DisplayableError`.\n\n## Results and errors\n\nFallible operations return a discriminated result rather than throwing:\n\n```ts\ntype DomainResult<T> =\n  | { ok: true; data: T }\n  | { ok: false; errors: DisplayableError[] };\n\ntype DomainVoidResult =\n  | { ok: true }\n  | { ok: false; errors: DisplayableError[] };\n\ntype DisplayableError = { field?: string[] | null; message: string };\n```\n\n`create`, `update` (both static and instance), and `reconcileChildren` return\n`DomainResult`. The customer-list helpers return `DomainVoidResult`. These are\nmade to be returned straight out of a server function: no wrapping needed.\n\n`deleteMany` is different because a bulk delete can partially fail. It returns a\nper-id breakdown:\n\n```ts\ntype DeleteManyResult = {\n  ok: boolean; // true only if every id deleted\n  results: Array<\n    | { id: string; ok: true }\n    | { id: string; ok: false; error: DisplayableError }\n  >;\n};\n```\n\nTwo helpers ease the display side:\n\n```ts\nimport { firstError, toDisplayableErrors } from \"@by-association-only/metaobjects\";\n\n// Pull one message out for a toast or banner.\nif (!result.ok) toast(firstError(result));\n\n// Turn raw strings into DisplayableError[].\ntoDisplayableErrors([\"Something went wrong\"]);\n// [{ field: null, message: \"Something went wrong\" }]\n```\n\n## Gotchas\n\n- **`reconcileChildren` does not roll back.** A failed phase leaves earlier\n  writes in place. Treat it as re-runnable, not transactional. Fix the cause and\n  call it again.\n- **Empty string deserialises to `null`.** A blank Shopify field comes back as\n  `null` even for a field the schema types as required (`string`). The type says\n  `string`; the runtime value can be `null`. Guard fields that might be blank.\n- **`deleteMany` returns `DeleteManyResult`, not `DomainResult`.** Check\n  `result.ok` for the all-or-nothing view, and walk `result.results` to find the\n  ids that failed. Do not reach for `result.data` or `result.errors`; they are\n  not there.\n- **`upsert` is keyed on the handle you pass, not the search index.** Shopify\n  matches on `type` + `handle` server-side, so an upsert issued milliseconds\n  after a write behaves consistently. Looking a record up first with\n  `all(client, { query: 'fields.slug:\"...\"' })` does not: that query is backed\n  by the search index and is eventually consistent, so a record created moments\n  ago may not come back yet. Mint a deterministic handle from your own domain\n  key and call `upsert` unconditionally.\n- **`optional` has to be the literal `true`.** `{ optional: true }`, not\n  `optional: someBoolean`. The split between required and optional keys is\n  driven by the literal `true`, so a widened boolean breaks it.\n- **Admin types come from a versioned subpath.**\n  `MetaobjectStatus`, `DisplayableError`, and the rest of the Admin schema types\n  come from `@by-association-only/shopify-admin-types/2026-07`. The schema\n  version is pinned in the subpath; there is no root export. This package\n  re-exports `MetaobjectStatus` and `DisplayableError` from its own barrel, so\n  import them from `@by-association-only/metaobjects` and you get the pinned\n  version without naming the subpath yourself.\n- **A subclass sent over a serialisation boundary must re-register itself.**\n  `defineMetaobject` registers the anonymous base class it returns. When your\n  subclass adds prototype members (a getter, a method) and instances cross a\n  boundary that reconstructs them from the registry (for example TanStack Start\n  server functions, where a class instance is serialised and rebuilt on the\n  other side), the rebuild picks the registered class. If that is the base and\n  not your subclass, the prototype members vanish. abask's `Registry` handles\n  this by re-registering the subclass under the same type after declaring it:\n\n  ```ts\n  import { registerMetaobject } from \"@by-association-only/metaobjects\";\n\n  export class Registry extends defineMetaobject(registryType, registrySchema) {\n    get shippingAddress() { /* ... */ }\n  }\n\n  // Re-register the subclass so wire-deserialised instances keep Registry's\n  // members. Without this the `shippingAddress` getter vanishes on the client.\n  registerMetaobject(registryType, Registry);\n  ```\n\n  Plain data access (`.fields`, `.id`) survives regardless; only the added\n  prototype members are at risk, so you only need this when the subclass has\n  them and its instances cross such a boundary.\n","readmeFilename":"README.md"}