Highly ambitious ATProtocol AppView service and sdks

use orWhere instead $or in generated client for better type safety

+106 -33
+66 -14
api/scripts/generate_typescript.ts
··· 466 466 ], 467 467 }); 468 468 469 - // Where clause type with OR support 469 + // Where clause type - simple mapped type for direct use 470 470 sourceFile.addTypeAlias({ 471 471 name: "WhereClause", 472 472 typeParameters: [{ name: "T", constraint: "string", default: "string" }], 473 473 isExported: true, 474 - type: "Partial<Record<T, WhereCondition>> & { $or?: Partial<Record<T, WhereCondition>> }", 474 + type: "{ [K in T]?: WhereCondition }", 475 475 }); 476 + 476 477 477 478 // IndexedRecord fields that are always available for filtering 478 479 sourceFile.addTypeAlias({ ··· 494 495 ], 495 496 }); 496 497 497 - // Unified params for single endpoint 498 + // Unified params for single endpoint with separate where and orWhere 498 499 sourceFile.addInterface({ 499 500 name: "SliceRecordsParams", 500 501 typeParameters: [ ··· 507 508 { name: "cursor", type: "string", hasQuestionToken: true }, 508 509 { 509 510 name: "where", 510 - type: "WhereClause<TSortField | IndexedRecordFields>", 511 + type: "{ [K in TSortField | IndexedRecordFields]?: WhereCondition }", 512 + hasQuestionToken: true, 513 + }, 514 + { 515 + name: "orWhere", 516 + type: "{ [K in TSortField | IndexedRecordFields]?: WhereCondition }", 511 517 hasQuestionToken: true, 512 518 }, 513 519 { ··· 518 524 ], 519 525 }); 520 526 521 - // Slice-level params (allows any field name for cross-collection queries) 527 + // Slice-level params with generic type support for field names 522 528 sourceFile.addInterface({ 523 529 name: "SliceLevelRecordsParams", 530 + typeParameters: [{ name: "TRecord", default: "Record<string, unknown>" }], 524 531 isExported: true, 525 532 properties: [ 526 533 { name: "slice", type: "string" }, ··· 528 535 { name: "cursor", type: "string", hasQuestionToken: true }, 529 536 { 530 537 name: "where", 531 - type: "WhereClause<string>", 538 + type: "{ [K in keyof TRecord | IndexedRecordFields]?: WhereCondition }", 539 + hasQuestionToken: true, 540 + }, 541 + { 542 + name: "orWhere", 543 + type: "{ [K in keyof TRecord | IndexedRecordFields]?: WhereCondition }", 532 544 hasQuestionToken: true, 533 545 }, 534 546 { ··· 1295 1307 parameters: [ 1296 1308 { 1297 1309 name: "params", 1298 - type: `{ limit?: number; cursor?: string; where?: WhereClause<${whereFieldsType}>; sortBy?: SortField<${sortFieldsType}>[]; }`, 1310 + type: `{ limit?: number; cursor?: string; where?: { [K in ${whereFieldsType}]?: WhereCondition }; orWhere?: { [K in ${whereFieldsType}]?: WhereCondition }; sortBy?: SortField<${sortFieldsType}>[]; }`, 1299 1311 hasQuestionToken: true, 1300 1312 }, 1301 1313 ], ··· 1311 1323 parameters: [ 1312 1324 { 1313 1325 name: "params", 1314 - type: `{ limit?: number; cursor?: string; where?: WhereClause<${whereFieldsType}>; sortBy?: SortField<${sortFieldsType}>[]; }`, 1326 + type: `{ limit?: number; cursor?: string; where?: { [K in ${whereFieldsType}]?: WhereCondition }; orWhere?: { [K in ${whereFieldsType}]?: WhereCondition }; sortBy?: SortField<${sortFieldsType}>[]; }`, 1315 1327 hasQuestionToken: true, 1316 1328 }, 1317 1329 ], ··· 1423 1435 if (method.name === "getRecords") { 1424 1436 const recordType = obj._recordType as string; 1425 1437 methodDecl.addStatements([ 1426 - `const requestParams = { ...params, slice: this.sliceUri };`, 1438 + `// Combine where and orWhere into the expected backend format`, 1439 + `const whereClause: any = params?.where ? { ...params.where } : {};`, 1440 + `if (params?.orWhere) {`, 1441 + ` whereClause.$or = params.orWhere;`, 1442 + `}`, 1443 + `const requestParams = {`, 1444 + ` ...params,`, 1445 + ` where: Object.keys(whereClause).length > 0 ? whereClause : undefined,`, 1446 + ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1447 + ` slice: this.sliceUri`, 1448 + `};`, 1427 1449 `const result = await this.makeRequest<SliceRecordsOutput>('${collectionPath}.getRecords', 'POST', requestParams);`, 1428 1450 `return {`, 1429 1451 ` records: result.records.map(record => ({`, ··· 1469 1491 ]); 1470 1492 } else if (method.name === "countRecords") { 1471 1493 methodDecl.addStatements([ 1472 - `const requestParams = { ...params, slice: this.sliceUri };`, 1494 + `// Combine where and orWhere into the expected backend format`, 1495 + `const whereClause: any = params?.where ? { ...params.where } : {};`, 1496 + `if (params?.orWhere) {`, 1497 + ` whereClause.$or = params.orWhere;`, 1498 + `}`, 1499 + `const requestParams = {`, 1500 + ` ...params,`, 1501 + ` where: Object.keys(whereClause).length > 0 ? whereClause : undefined,`, 1502 + ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1503 + ` slice: this.sliceUri`, 1504 + `};`, 1473 1505 `return await this.makeRequest<CountRecordsResponse>('${collectionPath}.countRecords', 'POST', requestParams);`, 1474 1506 ]); 1475 1507 } ··· 1506 1538 name: "getSliceRecords", 1507 1539 typeParameters: [{ name: "T", default: "Record<string, unknown>" }], 1508 1540 parameters: [ 1509 - { name: "params", type: "Omit<SliceLevelRecordsParams, 'slice'>" }, 1541 + { name: "params", type: "Omit<SliceLevelRecordsParams<T>, 'slice'>" }, 1510 1542 ], 1511 1543 returnType: "Promise<SliceRecordsOutput<T>>", 1512 1544 isAsync: true, 1513 1545 statements: [ 1514 - `const requestParams = { ...params, slice: this.sliceUri };`, 1546 + `// Combine where and orWhere into the expected backend format`, 1547 + `const whereClause: any = params?.where ? { ...params.where } : {};`, 1548 + `if (params?.orWhere) {`, 1549 + ` whereClause.$or = params.orWhere;`, 1550 + `}`, 1551 + `const requestParams = {`, 1552 + ` ...params,`, 1553 + ` where: Object.keys(whereClause).length > 0 ? whereClause : undefined,`, 1554 + ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1555 + ` slice: this.sliceUri`, 1556 + `};`, 1515 1557 `return await this.makeRequest<SliceRecordsOutput<T>>('social.slices.slice.getSliceRecords', 'POST', requestParams);`, 1516 1558 ], 1517 1559 }); ··· 1612 1654 name: "getSliceRecords", 1613 1655 typeParameters: [{ name: "T", default: "Record<string, unknown>" }], 1614 1656 parameters: [ 1615 - { name: "params", type: "Omit<SliceLevelRecordsParams, 'slice'>" }, 1657 + { name: "params", type: "Omit<SliceLevelRecordsParams<T>, 'slice'>" }, 1616 1658 ], 1617 1659 returnType: "Promise<SliceRecordsOutput<T>>", 1618 1660 isAsync: true, 1619 1661 statements: [ 1620 - `const requestParams = { ...params, slice: this.sliceUri };`, 1662 + `// Combine where and orWhere into the expected backend format`, 1663 + `const whereClause: any = params?.where ? { ...params.where } : {};`, 1664 + `if (params?.orWhere) {`, 1665 + ` whereClause.$or = params.orWhere;`, 1666 + `}`, 1667 + `const requestParams = {`, 1668 + ` ...params,`, 1669 + ` where: Object.keys(whereClause).length > 0 ? whereClause : undefined,`, 1670 + ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1671 + ` slice: this.sliceUri`, 1672 + `};`, 1621 1673 `return await this.makeRequest<SliceRecordsOutput<T>>('social.slices.slice.getSliceRecords', 'POST', requestParams);`, 1622 1674 ], 1623 1675 });
+40 -19
docs/sdk-usage.md
··· 142 142 }, 143 143 }); 144 144 145 + // Count with OR conditions 146 + const orCount = await client.com.example.post.countRecords({ 147 + where: { 148 + createdAt: { eq: "2025-09-03" }, 149 + }, 150 + orWhere: { 151 + title: { contains: "typescript" }, 152 + did: { eq: "did:plc:author" }, 153 + }, 154 + }); 155 + 145 156 console.log(`Found ${filteredCount.count} matching posts`); 157 + console.log(`Found ${orCount.count} posts with OR conditions`); 146 158 ``` 147 159 148 160 ### Getting a Single Record ··· 456 468 457 469 ### OR Query Support 458 470 459 - You can use OR queries to find records that match any of multiple conditions using the `$or` syntax: 471 + You can use OR queries to find records that match any of multiple conditions using the separate `orWhere` parameter. This provides clean type safety and autocomplete for field names: 460 472 461 473 ```typescript 462 474 // Find posts by either user1 OR user2 463 475 const posts = await client.com.example.post.getRecords({ 464 - where: { 465 - "$or": { 466 - did: { in: ["did:plc:user1", "did:plc:user2"] } 467 - } 476 + orWhere: { 477 + did: { in: ["did:plc:user1", "did:plc:user2"] } 468 478 } 469 479 }); 470 480 471 481 // Find posts that either have "typescript" in title OR are by a specific user 472 482 const posts = await client.com.example.post.getRecords({ 473 - where: { 474 - "$or": { 475 - title: { contains: "typescript" }, 476 - did: { eq: "did:plc:alice" } 477 - } 483 + orWhere: { 484 + title: { contains: "typescript" }, 485 + did: { eq: "did:plc:alice" } 478 486 } 479 487 }); 480 488 481 489 // Combining OR with regular AND conditions 482 490 const posts = await client.com.example.post.getRecords({ 483 491 where: { 484 - createdAt: { eq: "2025-09-03" }, // AND condition 485 - "$or": { // OR conditions 486 - title: { contains: "guide" }, 487 - did: { eq: "did:plc:user1" } 488 - } 492 + createdAt: { eq: "2025-09-03" }, // AND conditions 493 + }, 494 + orWhere: { // OR conditions 495 + title: { contains: "guide" }, 496 + did: { eq: "did:plc:user1" } 489 497 } 490 498 }); 491 499 // SQL: WHERE created_at = '2025-09-03' AND (title LIKE '%guide%' OR did = 'did:plc:user1') ··· 494 502 const crossCollectionOrSearch = await client.social.slices.slice.getSliceRecords({ 495 503 where: { 496 504 collection: { eq: "com.example.post" }, 497 - "$or": { 498 - title: { contains: "javascript" }, 499 - tags: { contains: "tutorial" } 500 - } 505 + }, 506 + orWhere: { 507 + title: { contains: "javascript" }, 508 + tags: { contains: "tutorial" } 509 + } 510 + }); 511 + 512 + // You get full autocomplete and type safety for field names in both where and orWhere 513 + const typedSearch = await client.com.example.post.getRecords({ 514 + where: { 515 + // TypeScript autocompletes valid field names here 516 + title: { contains: "react" }, 517 + }, 518 + orWhere: { 519 + // And also provides autocomplete here 520 + description: { contains: "tutorial" }, 521 + tags: { contains: "guide" }, 501 522 } 502 523 }); 503 524 ```